#!/usr/bin/env python
# coding=utf-8
"""
渠道效果分析可视化
输出交互式 HTML 报告，包含多个矩阵视图
"""
import pandas as pd
import json
from pathlib import Path

task_dir = Path(__file__).parent
output_dir = task_dir / "output"

# 找到最新的原始数据文件
csv_files = [f for f in output_dir.glob("*.csv") if f.stem.count('_') == 1]
if not csv_files:
    print("没有找到数据文件，请先运行 query.sql")
    exit(1)

latest_file = max(csv_files, key=lambda x: x.stat().st_mtime)
df = pd.read_csv(latest_file)

# 兼容旧版列名：自动映射到新名称
column_mapping = {
    '进入推荐率': '进入分发率',
    '再分享回流率': '整体裂变率',
    '原视频再分享回流率': '头部裂变率',
    '推荐再分享回流率': '推荐裂变率',
    '再分享回流uv': '裂变uv'
}
df = df.rename(columns={k: v for k, v in column_mapping.items() if k in df.columns})

print(f"分析文件: {latest_file.name}")
print(f"时间范围: {df['dt'].min()} ~ {df['dt'].max()}")

# ============================================================
# 日期列表
# ============================================================

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,
            '推荐裂变率': (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),
        'recommend': cc2.pivot_table(index='cat2_label', columns='channel', values='进入分发率'),
    }

# 为每个日期计算矩阵数据
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(
    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()

pivot_ror = channel_category.pivot(index='merge一级品类', columns='channel', values='整体裂变率')
pivot_ror_orig = 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)

# ============================================================
# 单渠道分析数据准备
# ============================================================

# 获取主要渠道列表（按UV排序）
main_channel_list = channel_stats['channel'].tolist()

# 计算每个渠道每天的数据（时间趋势）
def calc_channel_daily_trend(channel):
    """计算单渠道的每日趋势数据"""
    ch_df = df[df['channel'] == channel]
    trend = ch_df.groupby('dt').apply(
        lambda x: pd.Series({
            '点击uv': x['点击uv'].sum(),
            '裂变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 trend

# 计算每个渠道的品类分布
def calc_channel_categories(channel, date=None):
    """计算单渠道的品类分布，可按日期筛选"""
    ch_df = df[df['channel'] == channel]
    if date and date != 'all':
        ch_df = ch_df[ch_df['dt'].astype(str) == str(date)]
    cats = ch_df.groupby('merge一级品类').apply(
        lambda x: pd.Series({
            '点击uv': x['点击uv'].sum(),
            '裂变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 cats.sort_values('点击uv', ascending=False)

# 计算单渠道的品类×时间矩阵（支持一级/二级品类）
def calc_channel_category_time(channel, level='cat1'):
    """计算单渠道下各品类每天的数据"""
    ch_df = df[df['channel'] == channel]

    if level == 'cat1':
        ct = ch_df.groupby(['merge一级品类', 'dt']).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()
        cat_col = 'merge一级品类'
    else:
        ct = ch_df.groupby(['merge一级品类', 'merge二级品类', 'dt']).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()
        ct['cat2_label'] = ct.apply(
            lambda r: f"{str(r['merge一级品类'])}/{str(r['merge二级品类'])}"
            if pd.notna(r['merge二级品类']) else str(r['merge一级品类']), axis=1
        )
        cat_col = 'cat2_label'

    # 转为矩阵格式
    uv_pivot = ct.pivot(index=cat_col, columns='dt', values='点击uv').fillna(0)
    rec_pivot = ct.pivot(index=cat_col, columns='dt', values='进入分发率')
    ror_pivot = ct.pivot(index=cat_col, columns='dt', values='整体裂变率')
    orig_pivot = ct.pivot(index=cat_col, columns='dt', values='头部裂变率')
    rec_ror_pivot = ct.pivot(index=cat_col, columns='dt', values='推荐裂变率')

    # 按总UV排序
    all_cats = uv_pivot.sum(axis=1).sort_values(ascending=False).index.tolist()
    dates = sorted([int(d) for d in uv_pivot.columns])

    def to_dict(pivot, is_int=False):
        return {str(cat): {int(d): int(pivot.loc[cat, d]) if is_int else float(pivot.loc[cat, d]) if d in pivot.columns and pd.notna(pivot.loc[cat, d]) else 0 for d in dates} for cat in all_cats}

    return {
        'categories': all_cats,
        'dates': dates,
        'uv': to_dict(uv_pivot, is_int=True),
        'rec': to_dict(rec_pivot),
        'ror': to_dict(ror_pivot),
        'orig': to_dict(orig_pivot),
        'rec_ror': to_dict(rec_ror_pivot),
    }

# 计算整体平均数据（用于对比）
overall_avg = {
    '点击uv': df['点击uv'].sum() / len(main_channel_list),
    '进入分发率': (df['进入分发率'] * df['点击uv']).sum() / df['点击uv'].sum(),
    '整体裂变率': (df['整体裂变率'] * df['点击uv']).sum() / df['点击uv'].sum(),
    '头部裂变率': (df['头部裂变率'] * df['点击uv']).sum() / df['点击uv'].sum(),
    '推荐裂变率': (df['推荐裂变率'] * df['点击uv']).sum() / df['点击uv'].sum(),
}

# 预计算所有渠道的数据
channel_analysis_data = {}
for ch in main_channel_list:
    trend = calc_channel_daily_trend(ch)
    cats = calc_channel_categories(ch)
    cat_time_cat1 = calc_channel_category_time(ch, 'cat1')
    cat_time_cat2 = calc_channel_category_time(ch, 'cat2')
    ch_stats = channel_stats[channel_stats['channel'] == ch].iloc[0]
    # 计算每日品类数据
    daily_cats = {'all': cats.to_dict('records')}
    for dt in all_dates:
        dt_cats = calc_channel_categories(ch, dt)
        daily_cats[str(dt)] = dt_cats.to_dict('records')
    channel_analysis_data[ch] = {
        'trend': trend.to_dict('records'),
        'categories': cats.to_dict('records'),
        'daily_categories': daily_cats,
        'category_time': {'cat1': cat_time_cat1, 'cat2': cat_time_cat2},
        'summary': {
            '点击uv': int(ch_stats['点击uv']),
            '进入分发率': float(ch_stats['进入分发率']),
            '整体裂变率': float(ch_stats['整体裂变率']),
            '头部裂变率': float(ch_stats['头部裂变率']),
            '推荐裂变率': float(ch_stats['推荐裂变率']),
        }
    }

# 转为JSON供前端使用
channel_analysis_json = json.dumps(channel_analysis_data, ensure_ascii=False)
overall_avg_json = json.dumps(overall_avg, ensure_ascii=False)
channel_list_json = json.dumps(main_channel_list, ensure_ascii=False)

# ============================================================
# 统一渐变颜色函数
# ============================================================

def get_gradient_color(val, max_val, min_val=0):
    """统一的渐变颜色：白色(min) -> 绿色(max)"""
    if val is None or pd.isna(val) or val <= min_val:
        return "background: #f8f9fa"
    ratio = min((val - min_val) / (max_val - min_val), 1.0)
    r = int(255 - ratio * 215)
    g = int(255 - ratio * 88)
    b = int(255 - ratio * 186)
    return f"background: rgb({r},{g},{b})"

# ============================================================
# 表格HTML生成函数
# ============================================================

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:
                cells.append("<td>-</td>")
        rows.append("<tr>" + "".join(cells) + "</tr>")
    return "".join(rows)

# ============================================================
# 为每个日期生成表格HTML
# ============================================================

# 先计算汇总数据确定有效品类和渠道（用于筛选，不用于排序）
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_set = set(all_matrix['uv'][all_matrix['uv'].sum(axis=1) >= 1000].index.tolist())

# 二级品类有效列表
all_matrix2 = daily_data['all']['matrix2']
cat2_uv_sum = all_matrix2['uv'].sum(axis=1)
valid_cat2_set = set(cat2_uv_sum[cat2_uv_sum >= 1000].sort_values(ascending=False).head(20).index.tolist())

def get_sorted_cols(matrix_uv, valid_channels):
    """按当天各渠道总UV降序排列列"""
    col_uv = matrix_uv.sum(axis=0)
    valid_cols = [c for c in col_uv.index if c in valid_channels]
    return sorted(valid_cols, key=lambda c: col_uv.get(c, 0), reverse=True)

def get_sorted_rows(matrix_uv, valid_rows_set):
    """按当天各品类总UV降序排列行"""
    row_uv = matrix_uv.sum(axis=1)
    valid_rows = [r for r in row_uv.index if r in valid_rows_set]
    return sorted(valid_rows, key=lambda r: row_uv.get(r, 0), reverse=True)

def gen_header(label, cols):
    """生成表头"""
    return "<tr><th>" + label + "</th>" + "".join([f"<th>{c}</th>" for c in cols]) + "</tr>"

daily_html = {}
for dt in date_options:
    data = daily_data[dt]
    matrix = data['matrix']
    matrix2 = data['matrix2']

    # 当天按UV排序的列和行
    dt_cols = get_sorted_cols(matrix['uv'], main_channels)
    dt_rows = get_sorted_rows(matrix['uv'], valid_categories_set)
    dt_cols2 = get_sorted_cols(matrix2['uv'], main_channels)
    dt_rows2 = get_sorted_rows(matrix2['uv'], valid_cat2_set)

    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']),
        # 渠道×一级品类矩阵（按当天UV排序）
        'matrix_header': gen_header('品类', dt_cols),
        'matrix_ror': gen_matrix_rows(matrix['ror'], dt_rows, dt_cols, 0.8),
        'matrix_ror_orig': gen_matrix_rows(matrix['ror_orig'], dt_rows, dt_cols, 0.3),
        'matrix_ror_rec': gen_matrix_rows(matrix['ror_rec'], dt_rows, dt_cols, 0.5),
        'matrix_uv': gen_matrix_rows(matrix['uv'], dt_rows, dt_cols, 100000, is_int=True),
        'matrix_recommend': gen_matrix_rows(matrix['recommend'], dt_rows, dt_cols, 1.0, 0.6),
        # 渠道×二级品类矩阵（按当天UV排序）
        'matrix2_header': gen_header('二级品类', dt_cols2),
        'matrix2_ror': gen_matrix_rows(matrix2['ror'], dt_rows2, dt_cols2, 0.8),
        'matrix2_ror_orig': gen_matrix_rows(matrix2['ror_orig'], dt_rows2, dt_cols2, 0.3),
        'matrix2_ror_rec': gen_matrix_rows(matrix2['ror_rec'], dt_rows2, dt_cols2, 0.5),
        'matrix2_recommend': gen_matrix_rows(matrix2['recommend'], dt_rows2, dt_cols2, 1.0, 0.6),
        'matrix2_uv': gen_matrix_rows(matrix2['uv'], dt_rows2, dt_cols2, 100000, is_int=True),
    }

# ============================================================
# 生成 HTML
# ============================================================

total_uv = int(df['点击uv'].sum())
avg_ror = (df['整体裂变率'] * df['点击uv']).sum() / df['点击uv'].sum()
channel_count = df['channel'].nunique()
category_count = df['merge一级品类'].nunique()
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
channel_options = "".join([
    f'<option value="{ch}">{ch}</option>'
    for ch in main_channel_list
])

html_content = f"""<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>渠道效果分析报告</title>
    <style>
        body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
               margin: 0; background: #f5f5f5; }}
        .sidebar {{ position: fixed; left: 0; top: 0; width: 220px; height: 100vh;
                   background: #2c3e50; color: white; overflow-y: auto; padding: 20px 0;
                   box-shadow: 2px 0 5px rgba(0,0,0,0.1); }}
        .sidebar h2 {{ color: white; font-size: 16px; margin: 0 15px 15px; padding-bottom: 10px;
                      border-bottom: 1px solid #3d566e; }}
        .sidebar ul {{ list-style: none; margin: 0; padding: 0; }}
        .sidebar li {{ margin: 0; }}
        .sidebar a {{ display: block; padding: 8px 15px; color: #bdc3c7; text-decoration: none;
                     font-size: 13px; transition: all 0.2s; }}
        .sidebar a:hover {{ background: #34495e; color: white; }}
        .sidebar .sub a {{ padding-left: 30px; font-size: 12px; }}
        .sidebar .sub2 a {{ padding-left: 45px; font-size: 11px; color: #95a5a6; }}
        .container {{ margin-left: 240px; padding: 20px; max-width: 1400px; }}
        h1 {{ color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px; }}
        h2 {{ color: #444; margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; }}
        h3 {{ color: #555; margin-top: 20px; }}
        h4 {{ color: #666; margin: 15px 0 5px; font-size: 14px; }}
        .chart-container {{ background: white; padding: 20px; margin: 20px 0;
                           border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
        .summary {{ background: white; padding: 20px; margin: 20px 0;
                   border-radius: 8px; display: flex; gap: 20px; flex-wrap: wrap; }}
        .stat-card {{ flex: 1; min-width: 150px; padding: 15px;
                     background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                     border-radius: 8px; color: white; text-align: center; }}
        .stat-card h4 {{ margin: 0; font-size: 24px; color: white; }}
        .stat-card p {{ margin: 5px 0 0; opacity: 0.9; color: white; }}
        table {{ width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 13px; }}
        th, td {{ padding: 6px 10px; text-align: left; border-bottom: 1px solid #ddd; }}
        th {{ background: #f8f9fa; font-weight: 600; position: sticky; top: 0; }}
        tr:hover {{ background: #f5f5f5; }}
        .heatmap {{ overflow-x: auto; }}
        .heatmap td {{ text-align: center; min-width: 70px; }}
        canvas {{ max-height: 400px; }}
        .matrix-section {{ margin-bottom: 40px; }}
        .legend {{ font-size: 12px; margin: 10px 0; color: #666; }}
        .sortable th {{ cursor: pointer; user-select: none; }}
        .sortable th:hover {{ background: #e9ecef; }}
        .sortable td:first-child {{ cursor: pointer; }}
        .sortable td:first-child:hover {{ background: #e9ecef; }}
        .sort-hint {{ font-size: 10px; color: #999; margin-left: 5px; }}
        .reset-btn {{ font-size: 11px; padding: 2px 8px; margin-left: 10px; cursor: pointer;
                     background: #6c757d; color: white; border: none; border-radius: 3px; }}
        .reset-btn:hover {{ background: #5a6268; }}
        .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; }}
        .channel-selector {{ font-size: 14px; }}
        .channel-selector select {{ padding: 8px 15px; font-size: 14px; border: 1px solid #ccc;
                                   border-radius: 4px; min-width: 200px; }}
        .metric-selector {{ margin: 10px 0; display: flex; gap: 20px; flex-wrap: wrap; }}
        .metric-selector label {{ cursor: pointer; font-size: 13px; display: flex; align-items: center; gap: 5px; }}
        .comparison-table {{ width: 100%; }}
        .comparison-table th, .comparison-table td {{ text-align: center; padding: 10px; }}
        .comparison-table .positive {{ color: #28a745; font-weight: 600; }}
        .comparison-table .negative {{ color: #dc3545; font-weight: 600; }}
        #channel-summary .stat-card {{ min-width: 120px; }}
        .channel-selector-inline {{ margin-right: 15px; }}
        .channel-selector-inline select {{ padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 13px; }}
        </style>
</head>
<body>
    <nav class="sidebar">
        <h2>目录</h2>
        <ul>
            <li><a href="#sec1">一、整体表现</a></li>
            <li class="sub"><a href="#sec1-1">1.1 渠道整体</a></li>
            <li class="sub"><a href="#sec1-2">1.2 一级品类整体</a></li>
            <li class="sub"><a href="#sec1-3">1.3 二级品类整体</a></li>
            <li><a href="#sec2">二、渠道×一级品类</a></li>
            <li class="sub"><a href="#sec2-1">2.1 裂变率</a></li>
            <li class="sub2"><a href="#sec2-1-1">整体裂变率</a></li>
            <li class="sub2"><a href="#sec2-1-2">头部裂变率</a></li>
            <li class="sub2"><a href="#sec2-1-3">推荐裂变率</a></li>
            <li class="sub"><a href="#sec2-2">2.2 进入分发率</a></li>
            <li class="sub"><a href="#sec2-3">2.3 点击UV</a></li>
            <li><a href="#sec3">三、渠道×二级品类</a></li>
            <li class="sub"><a href="#sec3-1">3.1 裂变率</a></li>
            <li class="sub2"><a href="#sec3-1-1">整体裂变率</a></li>
            <li class="sub2"><a href="#sec3-1-2">头部裂变率</a></li>
            <li class="sub2"><a href="#sec3-1-3">推荐裂变率</a></li>
            <li class="sub"><a href="#sec3-2">3.2 进入分发率</a></li>
            <li class="sub"><a href="#sec3-3">3.3 点击UV</a></li>
            <li><a href="#sec4">四、单渠道分析</a></li>
            <li class="sub"><a href="#sec4-1">4.1 渠道选择</a></li>
            <li class="sub"><a href="#sec4-2">4.2 时间趋势</a></li>
            <li class="sub"><a href="#sec4-3">4.3 品类分布</a></li>
            <li class="sub"><a href="#sec4-4">4.4 品类×时间</a></li>
            <li class="sub"><a href="#sec4-5">4.5 指标对比</a></li>
        </ul>
    </nav>

    <div class="container">
        <h1>渠道效果分析报告</h1>
        <p>数据范围: {date_range}</p>

        <div class="summary">
            <div class="stat-card">
                <h4>{total_uv:,}</h4>
                <p>总点击UV</p>
            </div>
            <div class="stat-card">
                <h4>{avg_ror:.4f}</h4>
                <p>平均裂变率</p>
            </div>
            <div class="stat-card">
                <h4>{channel_count}</h4>
                <p>渠道数</p>
            </div>
            <div class="stat-card">
                <h4>{category_count}</h4>
                <p>品类数</p>
            </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>

        <h3 id="sec1-1">1.1 渠道整体</h3>
        <div class="chart-container" data-section="channel">
            <div class="legend">
                颜色越深=数值越高 | 点击表头排序 <button class="reset-btn" onclick="resetSection(this.closest('.chart-container').dataset.section)">重置</button>
                <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>

        <h3 id="sec1-2">1.2 一级品类整体</h3>
        <div class="chart-container" data-section="category">
            <div class="legend">
                颜色越深=数值越高 | 点击表头排序 <button class="reset-btn" onclick="resetSection(this.closest('.chart-container').dataset.section)">重置</button>
                <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>

        <h3 id="sec1-3">1.3 二级品类整体</h3>
        <div class="chart-container" data-section="cat2">
            <div class="legend">
                颜色越深=数值越高 | 点击表头排序 <button class="reset-btn" onclick="resetSection(this.closest('.chart-container').dataset.section)">重置</button>
                <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>

        <h2 id="sec2">二、渠道×一级品类</h2>

        <h3 id="sec2-1">2.1 裂变率</h3>
        <h4 id="sec2-1-1">整体裂变率</h4>
        <div class="chart-container heatmap matrix-section" data-section="m1_ror">
            <div class="legend">max=0.80 <button class="reset-btn" onclick="resetSection(this.closest('.chart-container').dataset.section)">重置</button>
                <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">{daily_html[dt]['matrix_header']}{daily_html[dt]['matrix_ror']}</table></div>''' for dt in date_options])}
        </div>

        <h4 id="sec2-1-2">头部裂变率</h4>
        <div class="chart-container heatmap matrix-section" data-section="m1_orig">
            <div class="legend">max=0.30 <button class="reset-btn" onclick="resetSection(this.closest('.chart-container').dataset.section)">重置</button>
                <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">{daily_html[dt]['matrix_header']}{daily_html[dt]['matrix_ror_orig']}</table></div>''' for dt in date_options])}
        </div>

        <h4 id="sec2-1-3">推荐裂变率</h4>
        <div class="chart-container heatmap matrix-section" data-section="m1_rec">
            <div class="legend">max=0.50 <button class="reset-btn" onclick="resetSection(this.closest('.chart-container').dataset.section)">重置</button>
                <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">{daily_html[dt]['matrix_header']}{daily_html[dt]['matrix_ror_rec']}</table></div>''' for dt in date_options])}
        </div>

        <h3 id="sec2-2">2.2 进入分发率</h3>
        <div class="chart-container heatmap matrix-section" data-section="m1_recommend">
            <div class="legend">min=0.60, max=1.00 <button class="reset-btn" onclick="resetSection(this.closest('.chart-container').dataset.section)">重置</button>
                <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">{daily_html[dt]['matrix_header']}{daily_html[dt]['matrix_recommend']}</table></div>''' for dt in date_options])}
        </div>

        <h3 id="sec2-3">2.3 点击UV</h3>
        <div class="chart-container heatmap matrix-section" data-section="m1_uv">
            <div class="legend">max=10万 <button class="reset-btn" onclick="resetSection(this.closest('.chart-container').dataset.section)">重置</button>
                <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">{daily_html[dt]['matrix_header']}{daily_html[dt]['matrix_uv']}</table></div>''' for dt in date_options])}
        </div>

        <h2 id="sec3">三、渠道×二级品类</h2>

        <h3 id="sec3-1">3.1 裂变率</h3>
        <h4 id="sec3-1-1">整体裂变率</h4>
        <div class="chart-container heatmap matrix-section" data-section="m2_ror">
            <div class="legend">max=0.80 <button class="reset-btn" onclick="resetSection(this.closest('.chart-container').dataset.section)">重置</button>
                <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">{daily_html[dt]['matrix2_header']}{daily_html[dt]['matrix2_ror']}</table></div>''' for dt in date_options])}
        </div>

        <h4 id="sec3-1-2">头部裂变率</h4>
        <div class="chart-container heatmap matrix-section" data-section="m2_orig">
            <div class="legend">max=0.30 <button class="reset-btn" onclick="resetSection(this.closest('.chart-container').dataset.section)">重置</button>
                <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">{daily_html[dt]['matrix2_header']}{daily_html[dt]['matrix2_ror_orig']}</table></div>''' for dt in date_options])}
        </div>

        <h4 id="sec3-1-3">推荐裂变率</h4>
        <div class="chart-container heatmap matrix-section" data-section="m2_rec">
            <div class="legend">max=0.50 <button class="reset-btn" onclick="resetSection(this.closest('.chart-container').dataset.section)">重置</button>
                <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">{daily_html[dt]['matrix2_header']}{daily_html[dt]['matrix2_ror_rec']}</table></div>''' for dt in date_options])}
        </div>

        <h3 id="sec3-2">3.2 进入分发率</h3>
        <div class="chart-container heatmap matrix-section" data-section="m2_recommend">
            <div class="legend">min=0.60, max=1.00 <button class="reset-btn" onclick="resetSection(this.closest('.chart-container').dataset.section)">重置</button>
                <div class="date-switcher table-date" data-section="m2_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">{daily_html[dt]['matrix2_header']}{daily_html[dt]['matrix2_recommend']}</table></div>''' for dt in date_options])}
        </div>

        <h3 id="sec3-3">3.3 点击UV</h3>
        <div class="chart-container heatmap matrix-section" data-section="m2_uv">
            <div class="legend">max=10万 <button class="reset-btn" onclick="resetSection(this.closest('.chart-container').dataset.section)">重置</button>
                <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">{daily_html[dt]['matrix2_header']}{daily_html[dt]['matrix2_uv']}</table></div>''' for dt in date_options])}
        </div>

        <h2 id="sec4">四、单渠道分析</h2>

        <h3 id="sec4-1">4.1 渠道选择</h3>
        <div class="chart-container">
            <div class="channel-selector">
                <label>选择渠道：</label>
                <select id="channel-select" onchange="syncChannelSelect(this.value)">
                    {"".join([f'<option value="{ch}">{ch}</option>' for ch in main_channel_list])}
                </select>
            </div>
            <div id="channel-summary" class="summary" style="margin-top: 15px;">
                <!-- 渠道汇总数据由JS动态填充 -->
            </div>
        </div>

        <h3 id="sec4-2">4.2 时间趋势</h3>
        <div class="chart-container">
            <div class="legend">
                <span class="channel-selector-inline">
                    渠道：<select class="channel-select-sync" onchange="syncChannelSelect(this.value)">{channel_options}</select>
                </span>
                选择指标查看趋势变化
            </div>
            <div class="metric-selector">
                <label><input type="checkbox" id="trend-uv" checked onchange="updateTrendChart()"> 点击UV</label>
                <label><input type="checkbox" id="trend-ror" checked onchange="updateTrendChart()"> 整体裂变率</label>
                <label><input type="checkbox" id="trend-rec" onchange="updateTrendChart()"> 进入分发率</label>
                <label><input type="checkbox" id="trend-orig" onchange="updateTrendChart()"> 头部裂变率</label>
                <label><input type="checkbox" id="trend-rec-ror" onchange="updateTrendChart()"> 推荐裂变率</label>
            </div>
            <canvas id="trend-chart" height="100"></canvas>
        </div>

        <h3 id="sec4-3">4.3 品类分布</h3>
        <div class="chart-container" data-section="channel-cat">
            <div class="legend">
                <span class="channel-selector-inline">
                    渠道：<select class="channel-select-sync" onchange="syncChannelSelect(this.value)">{channel_options}</select>
                </span>
                按点击UV排序
                <button class="reset-btn" onclick="resetCategoryTable()">重置</button>
                <div class="date-switcher table-date" data-section="channel-cat">
                    <button class="prev-btn">◀</button>
                    <select id="cat-date-select" class="date-select">{date_options_html}</select>
                    <button class="next-btn">▶</button>
                    <button class="play-btn">▶</button>
                </div>
            </div>
            <table id="channel-category-table" class="sortable">
                <thead>
                    <tr>
                        <th onclick="sortCategoryTable(0)">品类</th>
                        <th onclick="sortCategoryTable(1)">点击UV</th>
                        <th onclick="sortCategoryTable(2)">进入分发率</th>
                        <th onclick="sortCategoryTable(3)">整体裂变率</th>
                        <th onclick="sortCategoryTable(4)">头部裂变率</th>
                        <th onclick="sortCategoryTable(5)">推荐裂变率</th>
                    </tr>
                </thead>
                <tbody id="channel-category-body">
                    <!-- 由JS动态填充 -->
                </tbody>
            </table>
        </div>

        <h3 id="sec4-4">4.4 品类×时间</h3>
        <div class="chart-container">
            <div class="legend">
                <span class="channel-selector-inline">
                    渠道：<select class="channel-select-sync" onchange="syncChannelSelect(this.value)">{channel_options}</select>
                </span>
                品类：<select id="cat-time-level" onchange="updateCategoryTimeMatrix()" style="padding: 4px;">
                    <option value="cat1">一级品类</option>
                    <option value="cat2">二级品类</option>
                </select>
                指标：<select id="cat-time-metric" onchange="updateCategoryTimeMatrix()" style="padding: 4px;">
                    <option value="uv">点击UV</option>
                    <option value="rec">进入分发率</option>
                    <option value="ror">整体裂变率</option>
                    <option value="orig">头部裂变率</option>
                    <option value="rec_ror">推荐裂变率</option>
                </select>
                <button class="reset-btn" onclick="updateCategoryTimeMatrix()">重置</button>
            </div>
            <div class="heatmap">
                <table id="category-time-table">
                    <thead id="category-time-header">
                        <!-- 由JS动态填充 -->
                    </thead>
                    <tbody id="category-time-body">
                        <!-- 由JS动态填充 -->
                    </tbody>
                </table>
            </div>
        </div>

        <h3 id="sec4-5">4.5 指标对比</h3>
        <div class="chart-container">
            <div class="legend">
                <span class="channel-selector-inline">
                    渠道：<select class="channel-select-sync" onchange="syncChannelSelect(this.value)">{channel_options}</select>
                </span>
                vs 全渠道平均
            </div>
            <div id="comparison-container">
                <table class="comparison-table" id="comparison-table">
                    <thead>
                        <tr>
                            <th>指标</th>
                            <th>当前渠道</th>
                            <th>全渠道平均</th>
                            <th>差异</th>
                        </tr>
                    </thead>
                    <tbody id="comparison-body">
                        <!-- 由JS动态填充 -->
                    </tbody>
                </table>
            </div>
        </div>

    </div>

    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script>
    // 排序功能
    document.querySelectorAll('.sortable').forEach((table, tableIndex) => {{
        try {{
        const firstRow = table.querySelector('tr');
        if (!firstRow || !firstRow.cells) return;  // 跳过空表格

        const rows = Array.from(table.querySelectorAll('tbody tr, tr')).filter(tr => tr.querySelector('td'));
        const headerCells = table.querySelectorAll('th');
        if (rows.length === 0 || headerCells.length === 0) return;  // 无数据行或无表头

        // 保存原始顺序
        const originalRowOrder = rows.map(r => r.cloneNode(true));
        const originalColOrder = Array.from(firstRow.cells).map(c => c.cloneNode(true));

        // 点击表头：按列排序
        headerCells.forEach((th, colIndex) => {{
            if (!th) return;
            th.addEventListener('click', () => {{
                const isAsc = th.dataset.sort !== 'asc';
                headerCells.forEach(h => h.dataset.sort = '');
                th.dataset.sort = isAsc ? 'asc' : 'desc';

                rows.sort((a, b) => {{
                    const aCell = a.cells[colIndex];
                    const bCell = b.cells[colIndex];
                    if (!aCell || !bCell) return 0;

                    let aVal = aCell.textContent.replace(/,/g, '').replace(/-/g, '');
                    let bVal = bCell.textContent.replace(/,/g, '').replace(/-/g, '');

                    const aNum = parseFloat(aVal);
                    const bNum = parseFloat(bVal);

                    if (!isNaN(aNum) && !isNaN(bNum)) {{
                        return isAsc ? aNum - bNum : bNum - aNum;
                    }}
                    return isAsc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
                }});

                rows.forEach(row => table.appendChild(row));
            }});
        }});

        // 点击行首：按该行值重排列
        rows.forEach(row => {{
            const firstCell = row.cells[0];
            if (firstCell) {{
                firstCell.addEventListener('click', (e) => {{
                    e.stopPropagation();
                    const values = Array.from(row.cells);

                    const colData = [];
                    for (let i = 1; i < values.length; i++) {{
                        let val = values[i]?.textContent.replace(/,/g, '').replace(/-/g, '') || '0';
                        colData.push({{ index: i, value: parseFloat(val) || 0 }});
                    }}

                    colData.sort((a, b) => b.value - a.value);

                    table.querySelectorAll('tr').forEach(tr => {{
                        const cells = Array.from(tr.cells);
                        const first = cells[0];
                        const newRow = [first];
                        colData.forEach(col => newRow.push(cells[col.index]));
                        newRow.forEach(cell => tr.appendChild(cell));
                    }});
                }});
            }}
        }});

        // 重置按钮
        const resetBtn = document.getElementById('reset-' + tableIndex);
        if (resetBtn) {{
            resetBtn.addEventListener('click', () => {{
                // 重置行顺序
                const headerRow = table.querySelector('tr');
                originalRowOrder.forEach((origRow, i) => {{
                    const currentRow = rows[i];
                    Array.from(origRow.cells).forEach((cell, j) => {{
                        currentRow.cells[j].innerHTML = cell.innerHTML;
                        currentRow.cells[j].style.cssText = cell.style.cssText;
                    }});
                    table.appendChild(currentRow);
                }});
                // 重置列顺序
                table.querySelectorAll('tr').forEach((tr, rowIdx) => {{
                    if (rowIdx === 0) {{
                        originalColOrder.forEach((cell, j) => {{
                            tr.cells[j].innerHTML = cell.innerHTML;
                        }});
                    }}
                }});
                headerCells.forEach(h => h.dataset.sort = '');
                location.reload(); // 简单方案：刷新页面
            }});
        }}
        }} catch(e) {{ console.error('排序初始化错误', e); }}
    }});

    // ============================================================
    // 日期切换功能
    // ============================================================
    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_recommend', 'm1_uv',
            'm2_ror', 'm2_orig', 'm2_rec', 'm2_recommend', '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);
    }}

    // 保存每个表格的原始HTML（用于重置）
    const originalContent = {{}};
    document.querySelectorAll('.chart-container[data-section]').forEach(container => {{
        const section = container.dataset.section;
        originalContent[section] = {{}};
        container.querySelectorAll('.date-content').forEach(content => {{
            const date = content.dataset.date;
            originalContent[section][date] = content.innerHTML;
        }});
    }});

    // 重置单个表格排序（恢复原始HTML）
    function resetSection(section) {{
        const container = document.querySelector(`.chart-container[data-section="${{section}}"]`);
        if (!container || !originalContent[section]) return;
        container.querySelectorAll('.date-content').forEach(content => {{
            const date = content.dataset.date;
            if (originalContent[section][date]) {{
                content.innerHTML = originalContent[section][date];
            }}
        }});
    }}

    // 绑定全局切换器
    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;
        const isChannelCat = section === 'channel-cat';

        // 日期切换：根据 section 类型调用不同函数
        switcher.querySelector('.date-select').addEventListener('change', e => {{
            if (isChannelCat) {{
                updateCategoryTableWithAnimation();
            }} else {{
                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) {{
                select.value = dates[idx - 1];
                if (isChannelCat) {{
                    updateCategoryTableWithAnimation();
                }} else {{
                    switchDate(section, dates[idx - 1]);
                }}
            }}
            updateNavButtons(switcher);
        }});
        switcher.querySelector('.next-btn').addEventListener('click', () => {{
            const select = switcher.querySelector('.date-select');
            const idx = dates.indexOf(select.value);
            if (idx < dates.length - 1) {{
                select.value = dates[idx + 1];
                if (isChannelCat) {{
                    updateCategoryTableWithAnimation();
                }} else {{
                    switchDate(section, dates[idx + 1]);
                }}
            }}
            updateNavButtons(switcher);
        }});
        switcher.querySelector('.play-btn').addEventListener('click', () => {{
            if (isChannelCat) {{
                playCategorySection(switcher);
            }} else {{
                playSection(switcher, section);
            }}
        }});
        updateNavButtons(switcher);
    }});

    // ============================================================
    // 单渠道分析功能
    // ============================================================
    const channelData = {channel_analysis_json};
    const overallAvg = {overall_avg_json};
    const channelList = {channel_list_json};
    let trendChart = null;

    // 同步所有渠道选择器并更新分析
    function syncChannelSelect(channel) {{
        // 更新所有渠道选择器
        document.getElementById('channel-select').value = channel;
        document.querySelectorAll('.channel-select-sync').forEach(sel => {{
            sel.value = channel;
        }});
        // 更新分析
        updateChannelAnalysis(channel);
    }}

    function updateChannelAnalysis(channel) {{
        if (!channelData[channel]) return;
        const data = channelData[channel];

        // 更新汇总卡片
        const summary = data.summary;
        document.getElementById('channel-summary').innerHTML = `
            <div class="stat-card">
                <h4>${{summary['点击uv'].toLocaleString()}}</h4>
                <p>点击UV</p>
            </div>
            <div class="stat-card">
                <h4>${{summary['进入分发率'].toFixed(4)}}</h4>
                <p>进入分发率</p>
            </div>
            <div class="stat-card">
                <h4>${{summary['整体裂变率'].toFixed(4)}}</h4>
                <p>整体裂变率</p>
            </div>
            <div class="stat-card">
                <h4>${{summary['头部裂变率'].toFixed(4)}}</h4>
                <p>头部裂变率</p>
            </div>
            <div class="stat-card">
                <h4>${{summary['推荐裂变率'].toFixed(4)}}</h4>
                <p>推荐裂变率</p>
            </div>
        `;

        // 更新品类表格（调用独立函数）
        updateCategoryTable();

        // 更新对比表格
        const compBody = document.getElementById('comparison-body');
        const metrics = [
            {{ name: '点击UV', key: '点击uv', format: v => parseInt(v).toLocaleString() }},
            {{ name: '进入分发率', key: '进入分发率', format: v => v.toFixed(4) }},
            {{ name: '整体裂变率', key: '整体裂变率', format: v => v.toFixed(4) }},
            {{ name: '头部裂变率', key: '头部裂变率', format: v => v.toFixed(4) }},
            {{ name: '推荐裂变率', key: '推荐裂变率', format: v => v.toFixed(4) }},
        ];
        compBody.innerHTML = metrics.map(m => {{
            const chVal = summary[m.key];
            const avgVal = overallAvg[m.key];
            const diff = chVal - avgVal;
            const diffPct = avgVal !== 0 ? (diff / avgVal * 100).toFixed(1) : 0;
            const diffClass = diff > 0 ? 'positive' : (diff < 0 ? 'negative' : '');
            const diffSign = diff > 0 ? '+' : '';
            return `
                <tr>
                    <td>${{m.name}}</td>
                    <td>${{m.format(chVal)}}</td>
                    <td>${{m.format(avgVal)}}</td>
                    <td class="${{diffClass}}">${{diffSign}}${{diffPct}}%</td>
                </tr>
            `;
        }}).join('');

        // 更新趋势图和品类×时间矩阵
        updateTrendChart();
        updateCategoryTimeMatrix();
    }}

    // 渐变颜色函数
    function getGradient(val, maxVal, minVal = 0) {{
        if (val <= minVal || maxVal <= minVal) return '#f8f9fa';
        const ratio = Math.min((val - minVal) / (maxVal - minVal), 1);
        const r = Math.round(255 - ratio * 215);
        const g = Math.round(255 - ratio * 88);
        const b = Math.round(255 - ratio * 186);
        return `rgb(${{r}},${{g}},${{b}})`;
    }}

    // 品类分布表格 - 记录当前行顺序用于动画
    let catTableOrder = {{}};

    function getCatTableOrder() {{
        const tbody = document.getElementById('channel-category-body');
        const rows = tbody.querySelectorAll('tr');
        const order = {{}};
        rows.forEach((tr, idx) => {{
            const key = tr.cells[0]?.textContent || '';
            order[key] = idx;
        }});
        return order;
    }}

    function updateCategoryTable() {{
        const channel = document.getElementById('channel-select').value;
        const date = document.getElementById('cat-date-select').value;
        if (!channelData[channel] || !channelData[channel].daily_categories) return;

        const cats = channelData[channel].daily_categories[date] || channelData[channel].categories;
        if (!cats || cats.length === 0) return;

        // 计算最大值用于渐变
        const maxUv = Math.max(...cats.map(c => c['点击uv'] || 0));

        const tbody = document.getElementById('channel-category-body');
        tbody.innerHTML = cats.map(cat => {{
            const uv = cat['点击uv'] || 0;
            const uvBg = getGradient(uv, maxUv);
            const recBg = getGradient(cat['进入分发率'] || 0, 1.0, 0.6);
            const rorBg = getGradient(cat['整体裂变率'] || 0, 0.8);
            const origBg = getGradient(cat['头部裂变率'] || 0, 0.3);
            const recRorBg = getGradient(cat['推荐裂变率'] || 0, 0.5);
            return `
                <tr>
                    <td>${{cat['merge一级品类'] || '-'}}</td>
                    <td style="text-align:right;background:${{uvBg}}">${{parseInt(uv).toLocaleString()}}</td>
                    <td style="text-align:center;background:${{recBg}}">${{(cat['进入分发率'] || 0).toFixed(4)}}</td>
                    <td style="text-align:center;background:${{rorBg}}">${{(cat['整体裂变率'] || 0).toFixed(4)}}</td>
                    <td style="text-align:center;background:${{origBg}}">${{(cat['头部裂变率'] || 0).toFixed(4)}}</td>
                    <td style="text-align:center;background:${{recRorBg}}">${{(cat['推荐裂变率'] || 0).toFixed(4)}}</td>
                </tr>
            `;
        }}).join('');

        // 更新行顺序记录
        catTableOrder = getCatTableOrder();
    }}

    // 带动画的品类表格更新
    function updateCategoryTableWithAnimation() {{
        const oldOrder = catTableOrder;
        updateCategoryTable();
        const newOrder = getCatTableOrder();

        // 应用动画
        const tbody = document.getElementById('channel-category-body');
        const rows = tbody.querySelectorAll('tr');
        rows.forEach((tr, newIdx) => {{
            const key = tr.cells[0]?.textContent || '';
            const oldIdx = oldOrder[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');
            }}
        }});
    }}

    // 品类分布自动播放
    function playCategorySection(switcher) {{
        const playBtn = switcher.querySelector('.play-btn');
        const key = 'channel-cat';

        if (playIntervals[key]) {{
            clearInterval(playIntervals[key]);
            playIntervals[key] = null;
            playBtn.classList.remove('playing');
            playBtn.textContent = '▶';
            return;
        }}

        playBtn.classList.add('playing');
        playBtn.textContent = '⏸';

        const select = document.getElementById('cat-date-select');
        let idx = 0;

        const play = () => {{
            if (idx >= dates.length) {{
                clearInterval(playIntervals[key]);
                playIntervals[key] = null;
                playBtn.classList.remove('playing');
                playBtn.textContent = '▶';
                return;
            }}
            select.value = dates[idx];
            updateCategoryTableWithAnimation();
            updateNavButtons(switcher);
            idx++;
        }};

        play();
        playIntervals[key] = setInterval(play, 1500);
    }}

    function switchCatDate(delta) {{
        const select = document.getElementById('cat-date-select');
        const idx = dates.indexOf(select.value);
        const newIdx = idx + delta;
        if (newIdx >= 0 && newIdx < dates.length) {{
            select.value = dates[newIdx];
            updateCategoryTableWithAnimation();
        }}
    }}

    function resetCategoryTable() {{
        document.getElementById('cat-date-select').value = '{latest_date}';
        updateCategoryTable();
    }}

    // 品类分布表格排序
    let catSortCol = -1;
    let catSortAsc = true;

    function sortCategoryTable(colIndex) {{
        const tbody = document.getElementById('channel-category-body');
        const rows = Array.from(tbody.querySelectorAll('tr'));
        if (rows.length === 0) return;

        // 记录旧顺序
        const oldOrder = getCatTableOrder();

        // 切换排序方向
        if (catSortCol === colIndex) {{
            catSortAsc = !catSortAsc;
        }} else {{
            catSortCol = colIndex;
            catSortAsc = colIndex === 0; // 品类名默认升序，其他默认降序
        }}

        rows.sort((a, b) => {{
            const aCell = a.cells[colIndex];
            const bCell = b.cells[colIndex];
            if (!aCell || !bCell) return 0;

            let aVal = aCell.textContent.replace(/,/g, '');
            let bVal = bCell.textContent.replace(/,/g, '');
            const aNum = parseFloat(aVal);
            const bNum = parseFloat(bVal);

            if (!isNaN(aNum) && !isNaN(bNum)) {{
                return catSortAsc ? aNum - bNum : bNum - aNum;
            }}
            return catSortAsc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
        }});

        rows.forEach(row => tbody.appendChild(row));

        // 应用动画
        rows.forEach((tr, newIdx) => {{
            const key = tr.cells[0]?.textContent || '';
            const oldIdx = oldOrder[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');
            }}
        }});

        // 更新顺序记录
        catTableOrder = getCatTableOrder();
    }}

    function updateTrendChart() {{
        const channel = document.getElementById('channel-select').value;
        if (!channelData[channel]) return;
        const trend = channelData[channel].trend;

        const labels = trend.map(d => String(d.dt).slice(-4)); // 取日期后4位
        const datasets = [];

        if (document.getElementById('trend-uv').checked) {{
            datasets.push({{
                label: '点击UV',
                data: trend.map(d => d['点击uv']),
                borderColor: '#007bff',
                backgroundColor: 'rgba(0,123,255,0.1)',
                yAxisID: 'y',
                tension: 0.3,
            }});
        }}
        if (document.getElementById('trend-ror').checked) {{
            datasets.push({{
                label: '整体裂变率',
                data: trend.map(d => d['整体裂变率']),
                borderColor: '#28a745',
                yAxisID: 'y1',
                tension: 0.3,
            }});
        }}
        if (document.getElementById('trend-rec').checked) {{
            datasets.push({{
                label: '进入分发率',
                data: trend.map(d => d['进入分发率']),
                borderColor: '#17a2b8',
                yAxisID: 'y1',
                tension: 0.3,
            }});
        }}
        if (document.getElementById('trend-orig').checked) {{
            datasets.push({{
                label: '头部裂变率',
                data: trend.map(d => d['头部裂变率']),
                borderColor: '#ffc107',
                yAxisID: 'y1',
                tension: 0.3,
            }});
        }}
        if (document.getElementById('trend-rec-ror').checked) {{
            datasets.push({{
                label: '推荐裂变率',
                data: trend.map(d => d['推荐裂变率']),
                borderColor: '#dc3545',
                yAxisID: 'y1',
                tension: 0.3,
            }});
        }}

        if (trendChart) {{
            trendChart.destroy();
        }}

        const ctx = document.getElementById('trend-chart').getContext('2d');
        trendChart = new Chart(ctx, {{
            type: 'line',
            data: {{ labels, datasets }},
            options: {{
                responsive: true,
                interaction: {{ mode: 'index', intersect: false }},
                scales: {{
                    y: {{
                        type: 'linear',
                        display: datasets.some(d => d.yAxisID === 'y'),
                        position: 'left',
                        title: {{ display: true, text: '点击UV' }},
                    }},
                    y1: {{
                        type: 'linear',
                        display: datasets.some(d => d.yAxisID === 'y1'),
                        position: 'right',
                        title: {{ display: true, text: '比率' }},
                        grid: {{ drawOnChartArea: false }},
                    }},
                }},
                plugins: {{
                    legend: {{ position: 'top' }},
                }},
            }},
        }});
    }}

    function updateCategoryTimeMatrix() {{
        const channel = document.getElementById('channel-select').value;
        if (!channelData[channel] || !channelData[channel].category_time) return;

        const level = document.getElementById('cat-time-level').value;
        const metric = document.getElementById('cat-time-metric').value;
        const catTime = channelData[channel].category_time[level];
        if (!catTime) return;

        const data = catTime[metric];
        const categories = catTime.categories;
        const dates = catTime.dates;

        // 生成表头（可点击排序）
        const dateLabels = dates.map(d => String(d).slice(-4));
        const levelLabel = level === 'cat1' ? '一级品类' : '二级品类';
        document.getElementById('category-time-header').innerHTML = `
            <tr><th style="cursor:pointer" onclick="sortCatTimeTable(0)">${{levelLabel}}</th>${{dateLabels.map((d, i) =>
                `<th style="cursor:pointer" onclick="sortCatTimeTable(${{i+1}})">${{d}}</th>`).join('')}}</tr>
        `;

        // 根据指标设置渐变范围
        let maxVal = 0, minVal = 0;
        categories.forEach(cat => {{
            dates.forEach(dt => {{
                const val = data[cat]?.[String(dt)] || 0;
                if (val > maxVal) maxVal = val;
            }});
        }});
        // 设置最小阈值
        const thresholds = {{ uv: 10000, rec: 1.0, ror: 0.5, orig: 0.2, rec_ror: 0.3 }};
        if (metric === 'rec') minVal = 0.6;
        maxVal = Math.max(maxVal, thresholds[metric] || 0.5);

        // 生成数据行（行头可点击排序）
        document.getElementById('category-time-body').innerHTML = categories.map((cat, rowIdx) => {{
            const cells = dates.map(dt => {{
                const val = data[cat]?.[String(dt)] || 0;
                const ratio = maxVal > minVal ? Math.min((val - minVal) / (maxVal - minVal), 1) : 0;
                const r = Math.round(255 - ratio * 215);
                const g = Math.round(255 - ratio * 88);
                const b = Math.round(255 - ratio * 186);
                const bg = ratio > 0 ? `rgb(${{r}},${{g}},${{b}})` : '#f8f9fa';
                const display = metric === 'uv' ? parseInt(val).toLocaleString() : val.toFixed(4);
                return `<td style="background:${{bg}};text-align:center">${{display}}</td>`;
            }}).join('');
            return `<tr><td style="cursor:pointer" onclick="sortCatTimeTableByRow(${{rowIdx}})">${{cat}}</td>${{cells}}</tr>`;
        }}).join('');
    }}

    // 品类×时间表排序状态
    let catTimeSortCol = -1;
    let catTimeSortAsc = true;

    function sortCatTimeTable(colIndex) {{
        const table = document.getElementById('category-time-table');
        const tbody = document.getElementById('category-time-body');
        const rows = Array.from(tbody.querySelectorAll('tr'));

        // 切换排序方向
        if (catTimeSortCol === colIndex) {{
            catTimeSortAsc = !catTimeSortAsc;
        }} else {{
            catTimeSortCol = colIndex;
            catTimeSortAsc = false; // 默认降序
        }}

        rows.sort((a, b) => {{
            const aCell = a.cells[colIndex];
            const bCell = b.cells[colIndex];
            let aVal = aCell.textContent.replace(/,/g, '');
            let bVal = bCell.textContent.replace(/,/g, '');
            const aNum = parseFloat(aVal);
            const bNum = parseFloat(bVal);
            if (!isNaN(aNum) && !isNaN(bNum)) {{
                return catTimeSortAsc ? aNum - bNum : bNum - aNum;
            }}
            return catTimeSortAsc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
        }});

        rows.forEach(row => tbody.appendChild(row));
    }}

    function sortCatTimeTableByRow(rowIndex) {{
        const table = document.getElementById('category-time-table');
        const thead = document.getElementById('category-time-header');
        const tbody = document.getElementById('category-time-body');
        const headerRow = thead.querySelector('tr');
        const dataRows = Array.from(tbody.querySelectorAll('tr'));
        const targetRow = dataRows[rowIndex];
        if (!targetRow) return;

        // 获取该行各列的值和索引
        const colData = [];
        for (let i = 1; i < targetRow.cells.length; i++) {{
            let val = targetRow.cells[i].textContent.replace(/,/g, '');
            colData.push({{ index: i, value: parseFloat(val) || 0 }});
        }}
        colData.sort((a, b) => b.value - a.value);

        // 重排所有行的列
        [headerRow, ...dataRows].forEach(tr => {{
            const cells = Array.from(tr.cells);
            const first = cells[0];
            const newOrder = [first];
            colData.forEach(col => newOrder.push(cells[col.index]));
            newOrder.forEach(cell => tr.appendChild(cell));
        }});
    }}

    // 初始化：加载第一个渠道
    if (channelList.length > 0) {{
        try {{
            syncChannelSelect(channelList[0]);
        }} catch (e) {{
            console.error('初始化错误:', e);
            alert('初始化错误: ' + e.message);
        }}
    }}
    </script>
</body>
</html>
"""

html_file = output_dir / f"{latest_file.stem}_报告.html"
with open(html_file, 'w', encoding='utf-8') as f:
    f.write(html_content)

print(f"\nHTML 报告已生成: {html_file}")
print(f"包含矩阵: 回流率、点击UV、进入分发率")
