Ακατέργαστη πηγή
max / CF解题数据可视化 (Pro Max)

// ==UserScript==
// @name         CF解题数据可视化 (Pro Max)
// @name:en      codeforces analytics
// @namespace    https://codeforces.com/profile/tongwentao
// @version      2.3.0
// @description  全方位分析Codeforces做题数据:支持中英文切换,新增一键生成高清长图分享功能。
// @description:en Analyse Codeforces profiles with 8 dimensions, bilingual support, and high-res image export.
// @author       tongwentao
// @match        https://codeforces.com/profile/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @connect      greasyfork.org
// @require      https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js
// @require      https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // --- 国际化 (i18n) 配置 ---
    let currentLang = 'en';
    const i18n = {
        en: {
            loading: 'Loading Analytics (Pro Max)...',
            fetchError: 'Failed to load data, possibly rate-limited by CF. Please try again later.',
            titleText: 'Codeforces Analytics',
            btnSwitch: '中文',
            btnShare: 'Share (Export Image)',
            generating: 'Generating...',
            ratings: 'Problem Ratings',
            tags: 'Tags Solved',
            lang: 'Programming Language',
            verdict: 'Verdict Distribution',
            attempts: 'Average Attempts to AC',
            participant: 'Participant Type',
            performance: 'Execution Performance',
            memoryPerf: 'Memory Usage (KB vs Rating)',
            timeline: 'Activity Timeline (Monthly)',
            heatmap: 'Submission Heatmap',
            unsolved: 'Unsolved Problems (Total: {n})',
            submissions: 'Submissions',
            time: 'Time (ms)',
            memory: 'Memory (KB)',
            rating: 'Rating',
            points: 'Points Earned',
            streak: 'Max Streak',
            days: 'days',
            speed: 'Contest Speed Analysis',
            try1: '1 Try (One Shot)',
            try2: '2 Tries',
            try3_5: '3-5 Tries',
            tryMore: '> 5 Tries (Struggle)',
            genErrorConsole: 'Failed to generate image:',
            genErrorAlert: 'Sorry, failed to generate the image. Please check the console for errors.'
        },
        zh: {
            loading: '正在加载分析数据 (Pro Max)...',
            fetchError: '加载数据失败,可能是 CF 接口限流,请稍后再试。',
            titleText: 'CF 解题数据可视化',
            btnSwitch: 'English',
            btnShare: '分享 (生成长图)',
            generating: '正在生成...',
            ratings: '题目难度分布',
            tags: '题目标签分布',
            lang: '编程语言偏好',
            verdict: '提交结果分布',
            attempts: '平均 AC 尝试次数',
            participant: '参赛类型分布',
            performance: '执行性能分布 (时间 vs 难度)',
            memoryPerf: '内存使用分布 (KB vs 难度)',
            timeline: '刷题活跃度 (月度)',
            heatmap: '提交热力图',
            unsolved: '未解决题目 (总计: {n} 题)',
            submissions: '提交数',
            time: '运行时间 (ms)',
            memory: '内存 (KB)',
            rating: '难度',
            points: '获得分数',
            streak: '最长连续',
            days: '天',
            speed: '比赛速度分析',
            try1: '1 Try (一发入魂)',
            try2: '2 Tries',
            try3_5: '3-5 Tries',
            tryMore: '> 5 Tries (折磨)',
            genErrorConsole: '生成图片失败:',
            genErrorAlert: '抱歉,生成图片失败,请检查控制台报错。'
        }
    };

    const t = (key, params = {}) => {
        let text = i18n[currentLang][key] || key;
        for (const [k, v] of Object.entries(params)) {
            text = text.replace(`{${k}}`, v);
        }
        return text;
    };

    const chartInstances = [];

    window.addEventListener('resize', () => {
        chartInstances.forEach(chart => chart && chart.resize());
    });

    const getRatingColor = (rating) => {
        if (rating >= 3000) return '#aa0100';
        if (rating >= 2600) return '#ff3333';
        if (rating >= 2400) return '#ff7777';
        if (rating >= 2300) return '#ffbb55';
        if (rating >= 2100) return '#ffcc87';
        if (rating >= 1900) return '#ff88ff';
        if (rating >= 1600) return '#aaaaff';
        if (rating >= 1400) return '#76ddbb';
        if (rating >= 1200) return '#76ff77';
        return '#cccccc';
    };

    // --- DOM 渲染与容器管理 ---
    const initDashboardContainer = (res) => {
        let container = document.getElementById('cf-analytics-wrapper');
        if (!container) {
            container = document.createElement('div');
            container.id = 'cf-analytics-wrapper';
            container.style.cssText = 'margin-top: 2em; padding: 20px; background: #ffffff; border-radius: 8px; box-sizing: border-box;';
            document.getElementById('pageContent').appendChild(container);
        }

        container.innerHTML = `
            <div id="cf-header-bar" style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #ccc; padding-bottom: 10px; margin-bottom: 15px;">
                <h2 style="margin: 0; color: #3b5998; font-weight: bold;">${t('titleText')}</h2>
                <div style="display: flex; gap: 10px;">
                    <button id="cf-share-btn" style="padding: 5px 15px; cursor: pointer; border-radius: 4px; border: 1px solid #28a745; background: #28a745; color: white; font-weight: bold; transition: background 0.2s;">
                        ${t('btnShare')}
                    </button>
                    <button id="cf-lang-toggle" style="padding: 5px 15px; cursor: pointer; border-radius: 4px; border: 1px solid #0073e6; background: #0073e6; color: white; font-weight: bold; transition: background 0.2s;">
                        ${t('btnSwitch')}
                    </button>
                </div>
            </div>
            <div id="cf-analytics-dashboard" style="display: flex; flex-wrap: wrap; justify-content: space-between; gap: 1em 0;"></div>
        `;

        document.getElementById('cf-lang-toggle').addEventListener('click', () => {
            currentLang = currentLang === 'en' ? 'zh' : 'en';
            chartInstances.forEach(chart => chart && chart.dispose());
            chartInstances.length = 0;
            drawCharts(res);
        });

        // 分享截图逻辑 (带用户名注入与图例数据展示)
        document.getElementById('cf-share-btn').addEventListener('click', async () => {
            const btn = document.getElementById('cf-share-btn');
            const originalText = btn.innerText;
            btn.innerText = t('generating');
            btn.disabled = true;
            btn.style.opacity = '0.7';

            try {
                const wrapper = document.getElementById('cf-analytics-wrapper');
                const handle = window.location.pathname.split('/').pop();

                // 隐藏按钮
                const headerBtns = wrapper.querySelector('div[style*="gap: 10px"]');
                if (headerBtns) headerBtns.style.display = 'none';

                // 修改标题加入用户名
                const titleEl = wrapper.querySelector('h2');
                const originalTitle = titleEl.innerText;
                titleEl.innerText = `${originalTitle} @${handle}`;

                // 强制宽度
                const originalWidth = wrapper.style.width;
                const originalMaxWidth = wrapper.style.maxWidth;
                wrapper.style.width = '1200px';
                wrapper.style.maxWidth = '1200px';

                // 为截图修改图表样式:将数值放入图例
                chartInstances.forEach(chart => {
                    if (chart) {
                        const option = chart.getOption();
                        if (option.series && option.series[0] && option.series[0].type === 'pie') {
                            const data = option.series[0].data;
                            chart.setOption({
                                legend: {
                                    // 自定义图例文字,格式:名字: 数量
                                    formatter: function (name) {
                                        const item = data.find(d => d.name === name);
                                        return item ? `${name}: ${item.value}` : name;
                                    },
                                    textStyle: {
                                        width: 150, // 稍微拉宽防止数字被省略号截断
                                        overflow: 'truncate'
                                    }
                                },
                                series: [{ label: { show: false } }] // 确保外面的蜘蛛网线关闭
                            });
                        }
                        chart.resize();
                    }
                });

                await new Promise(r => setTimeout(r, 800));

                const canvas = await html2canvas(wrapper, {
                    scale: 2,
                    useCORS: true,
                    backgroundColor: '#ffffff',
                    width: 1200
                });

                // 恢复原状
                wrapper.style.width = originalWidth;
                wrapper.style.maxWidth = originalMaxWidth;
                titleEl.innerText = originalTitle;
                if (headerBtns) headerBtns.style.display = 'flex';

                chartInstances.forEach(chart => {
                    if (chart) {
                        const option = chart.getOption();
                        if (option.series && option.series[0] && option.series[0].type === 'pie') {
                            chart.setOption({
                                legend: {
                                    formatter: '{name}', // 恢复默认图例只显示名字
                                    textStyle: { width: 100, overflow: 'truncate' }
                                }
                            });
                        }
                        chart.resize();
                    }
                });

                // 触发下载
                const imgData = canvas.toDataURL('image/png');
                const a = document.createElement('a');
                a.href = imgData;
                a.download = `CF_Stats_${handle}.png`;
                a.click();

            } catch (err) {
                console.error(t('genErrorConsole'), err);
                alert(t('genErrorAlert'));
            } finally {
                btn.innerText = originalText;
                btn.disabled = false;
                btn.style.opacity = '1';
            }
        });
    };

    const createChartContainer = (id, widthStr = '48%') => {
        const isMobile = window.innerWidth < 800;
        const finalWidth = isMobile ? '100%' : widthStr;
        const div = `<div class="roundbox userActivityRoundBox borderTopRound borderBottomRound" id="${id}" style="width: ${finalWidth}; height:400px; padding:2em 1em 0 1em; box-sizing: border-box;"></div>`;
        document.getElementById('cf-analytics-dashboard').insertAdjacentHTML('beforeend', div);
        return document.getElementById(id);
    };

    // --- 图表渲染主逻辑 ---
    function drawCharts(res) {
        initDashboardContainer(res);

        drawTimelineChart('timelineChart', t('timeline'), res.timeline, '100%');
        drawBarChart('ratingChart', t('ratings'), res.rating, '100%');

        const localizedAttempts = {};
        for (const [key, value] of Object.entries(res.attempts)) {
            localizedAttempts[t(key)] = value;
        }

        drawPieChart('tagsChart', t('tags'), res.tags);
        drawPieChart('langChart', t('lang'), res.lang);
        drawPieChart('verdictChart', t('verdict'), res.verdicts);
        drawPieChart('attemptsChart', t('attempts'), localizedAttempts);
        drawPieChart('participantChart', t('participant'), res.participantType);
        drawScatterChart('performanceChart', t('performance'), res.performance, 'time');
        drawScatterChart('memoryChart', t('memoryPerf'), res.memoryPerformance, 'memory');
        drawSpeedChart('speedChart', t('speed'), res.speedAnalysis, '49%');

        drawStatsSummary(res.stats);
        drawUnsolvedChart(res.unsolved);
    }

    // --- 具体图表绘制函数 ---
    function drawBarChart(id, titleText, dataObj, width) {
        if (Object.keys(dataObj).length === 0) return;
        const chartDom = createChartContainer(id, width);
        const myChart = echarts.init(chartDom);
        chartInstances.push(myChart);

        const xData = Object.keys(dataObj).sort((a, b) => a - b);
        const yData = xData.map(key => dataObj[key]);

        myChart.setOption({
            title: { text: titleText, left: 'center' },
            tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
            grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
            xAxis: [{ type: 'category', data: xData, axisTick: { alignWithLabel: true } }],
            yAxis: [{ type: 'value' }],
            series: [{
                name: 'Solved', type: 'bar', barWidth: '60%',
                data: yData.map((value, index) => ({
                    value: value, itemStyle: { color: getRatingColor(Number(xData[index])) }
                }))
            }]
        });
    }

    function drawPieChart(id, titleText, dataObj) {
        if (Object.keys(dataObj).length === 0) return;
        const chartDom = createChartContainer(id, '49%');
        const myChart = echarts.init(chartDom);
        chartInstances.push(myChart);

        const dataArr = Object.entries(dataObj)
            .map(([name, value]) => ({ name, value }))
            .sort((a, b) => b.value - a.value);

        myChart.setOption({
            title: {
                text: titleText,
                left: 'center',
                top: 10,
                textStyle: { fontSize: 14 }
            },
            tooltip: { trigger: 'item', formatter: '{b} : {c} ({d}%)' },
            legend: {
                type: 'scroll',
                orient: 'vertical',
                right: '2%',
                top: 55,
                bottom: 20,
                width: '45%',
                textStyle: {
                    fontSize: 11,
                    width: 100,
                    overflow: 'truncate'
                },
                tooltip: { show: true }
            },
            series: [{
                type: 'pie',
                radius: ['35%', '55%'],
                center: ['33%', '55%'],
                itemStyle: { borderRadius: 5, borderColor: '#fff', borderWidth: 2 },
                data: dataArr,
                label: { show: false }, // 默认不显示,悬浮才高亮
                emphasis: { label: { show: true, fontSize: 12, fontWeight: 'bold' } }
            }]
        });
    }

    function drawTimelineChart(id, titleText, dataObj, width) {
        if (Object.keys(dataObj).length === 0) return;
        const chartDom = createChartContainer(id, width);
        const myChart = echarts.init(chartDom);
        chartInstances.push(myChart);

        const xData = Object.keys(dataObj).sort();
        const yData = xData.map(key => dataObj[key]);

        myChart.setOption({
            title: { text: titleText, left: 'center' },
            tooltip: { trigger: 'axis' },
            grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
            xAxis: { type: 'category', boundaryGap: false, data: xData },
            yAxis: { type: 'value', name: t('submissions') },
            dataZoom: [{ type: 'inside', start: 0, end: 100 }, { start: 0, end: 100 }],
            series: [{
                name: t('submissions'), type: 'line', smooth: true,
                areaStyle: { opacity: 0.3, color: '#0073e6' },
                lineStyle: { color: '#0073e6' }, itemStyle: { color: '#0073e6' },
                data: yData
            }]
        });
    }

    function drawScatterChart(id, titleText, dataArr, metric) {
        if (dataArr.length === 0) return;
        const chartDom = createChartContainer(id, '49%');
        const myChart = echarts.init(chartDom);
        chartInstances.push(myChart);

        const isMemory = metric === 'memory';
        const axisName = isMemory ? t('memory') : t('time');
        const color = isMemory ? 'rgba(40, 167, 69, 0.6)' : 'rgba(0, 115, 230, 0.6)';

        myChart.setOption({
            title: { text: titleText, left: 'center', textStyle: { fontSize: 14 } },
            grid: { left: '3%', right: '8%', bottom: '3%', containLabel: true },
            tooltip: {
                formatter: function (param) {
                    const data = param.data;
                    return `<div style="font-weight:bold;">${data[2]}</div>${axisName}: ${data[0]}<br/>${t('rating')}: ${data[1]}`;
                }
            },
            xAxis: { type: 'value', name: axisName, splitLine: { show: false } },
            yAxis: { type: 'value', name: t('rating'), splitLine: { show: false } },
            series: [{ symbolSize: 6, data: dataArr, type: 'scatter', itemStyle: { color: color } }]
        });
    }



    function drawSpeedChart(id, titleText, speedData, width) {
        if (!speedData || speedData.length === 0) return;
        const chartDom = createChartContainer(id, width);
        const myChart = echarts.init(chartDom);
        chartInstances.push(myChart);

        const categories = ['0-10min', '10-30min', '30-60min', '1-2h', '2-4h', '>4h'];
        const values = categories.map(cat => speedData[cat] || 0);

        myChart.setOption({
            title: { text: titleText, left: 'center', textStyle: { fontSize: 14 } },
            tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
            grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
            xAxis: { type: 'category', data: categories, axisTick: { alignWithLabel: true } },
            yAxis: { type: 'value', name: t('submissions') },
            series: [{
                name: t('submissions'),
                type: 'bar',
                data: values,
                itemStyle: {
                    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                        { offset: 0, color: '#83bff6' },
                        { offset: 0.5, color: '#188df0' },
                        { offset: 1, color: '#188df0' }
                    ])
                },
                emphasis: {
                    itemStyle: {
                        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                            { offset: 0, color: '#2378f7' },
                            { offset: 0.7, color: '#2378f7' },
                            { offset: 1, color: '#83bff6' }
                        ])
                    }
                }
            }]
        });
    }

    function drawStatsSummary(stats) {
        if (!stats) return;

        const div = `
            <div class="roundbox userActivityRoundBox borderTopRound borderBottomRound" style="width: 100%; padding: 1.5em; margin-top: 1em; box-sizing: border-box;">
                <h4 style="font-size: 1.2em; color: #333; font-weight: bold; margin-bottom: 1em;">📊 ${currentLang === 'zh' ? '统计摘要' : 'Statistics Summary'}</h4>
                <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
                    <div style="background: #f8f9fa; padding: 15px; border-radius: 6px; border-left: 4px solid #0073e6;">
                        <div style="font-size: 0.9em; color: #666; margin-bottom: 5px;">${currentLang === 'zh' ? '总提交数' : 'Total Submissions'}</div>
                        <div style="font-size: 1.8em; font-weight: bold; color: #0073e6;">${stats.totalSubmissions || 0}</div>
                    </div>
                    <div style="background: #f8f9fa; padding: 15px; border-radius: 6px; border-left: 4px solid #28a745;">
                        <div style="font-size: 0.9em; color: #666; margin-bottom: 5px;">${currentLang === 'zh' ? '已解决题目' : 'Solved Problems'}</div>
                        <div style="font-size: 1.8em; font-weight: bold; color: #28a745;">${stats.solvedProblems || 0}</div>
                    </div>
                    <div style="background: #f8f9fa; padding: 15px; border-radius: 6px; border-left: 4px solid #ffc107;">
                        <div style="font-size: 0.9em; color: #666; margin-bottom: 5px;">${t('streak')}</div>
                        <div style="font-size: 1.8em; font-weight: bold; color: #ffc107;">${stats.maxStreak || 0} ${t('days')}</div>
                    </div>
                    <div style="background: #f8f9fa; padding: 15px; border-radius: 6px; border-left: 4px solid #dc3545;">
                        <div style="font-size: 0.9em; color: #666; margin-bottom: 5px;">${t('points')}</div>
                        <div style="font-size: 1.8em; font-weight: bold; color: #dc3545;">${stats.totalPoints || 0}</div>
                    </div>
                    <div style="background: #f8f9fa; padding: 15px; border-radius: 6px; border-left: 4px solid #6f42c1;">
                        <div style="font-size: 0.9em; color: #666; margin-bottom: 5px;">${currentLang === 'zh' ? 'AC率' : 'AC Rate'}</div>
                        <div style="font-size: 1.8em; font-weight: bold; color: #6f42c1;">${stats.acRate ? stats.acRate.toFixed(1) + '%' : '0%'}</div>
                    </div>
                    <div style="background: #f8f9fa; padding: 15px; border-radius: 6px; border-left: 4px solid #fd7e14;">
                        <div style="font-size: 0.9em; color: #666; margin-bottom: 5px;">${currentLang === 'zh' ? '最高难度' : 'Highest Rating'}</div>
                        <div style="font-size: 1.8em; font-weight: bold; color: #fd7e14;">${stats.highestRating || '-'}</div>
                    </div>
                </div>
            </div>`;
        document.getElementById('cf-analytics-dashboard').insertAdjacentHTML('beforeend', div);
    }

    function drawUnsolvedChart(unsolvedData) {
        const unsolvedKeys = Object.keys(unsolvedData);
        if (unsolvedKeys.length === 0) return;

        const div = `
            <div class="roundbox userActivityRoundBox borderTopRound borderBottomRound" style="width: 100%; padding: 1.5em; margin-top: 1em; box-sizing: border-box;">
                <h4 style="font-size: 1.2em; color: #333; font-weight: bold; margin-bottom: 0.5em;">${t('unsolved', { n: unsolvedKeys.length })}</h4>
                <div style="display: flex; flex-wrap: wrap; gap: 8px;">
                    ${Object.entries(unsolvedData).map(([id, info]) => {
                        const baseUrl = info.contestId < 10000
                            ? `https://codeforces.com/problemset/problem/${info.contestId}/${info.problemIndex}`
                            : `https://codeforces.com/problemset/gymProblem/${info.contestId}/${info.problemIndex}`;
                        return `<a href="${baseUrl}" target="_blank" style="text-decoration: none; color: #d9534f; background: #fdf0ef; padding: 4px 8px; border-radius: 4px; border: 1px solid #d9534f; font-size: 0.85em;">${id}</a>`;
                    }).join('')}
                </div>
            </div>`;
        document.getElementById('cf-analytics-dashboard').insertAdjacentHTML('beforeend', div);
    }

    // --- 数据处理逻辑 ---
    function processSubmissions(submissions) {
        submissions.sort((a, b) => a.creationTimeSeconds - b.creationTimeSeconds);

        const res = {
            rating: {},
            tags: {},
            lang: {},
            unsolved: {},
            verdicts: {},
            participantType: {},
            attempts: {},
            timeline: {},
            performance: [],
            memoryPerformance: [],
            speedAnalysis: {},
            stats: {
                totalSubmissions: 0,
                solvedProblems: 0,
                maxStreak: 0,
                totalPoints: 0,
                acRate: 0,
                highestRating: 0
            }
        };
        const problemState = new Map();
        const solvedProblems = new Map();
        const dailySubmissions = new Map();
        let totalAC = 0;
        let totalSubmissions = submissions.length;

        submissions.forEach(sub => {
            const problem = sub.problem;
            if (!problem || !problem.contestId) return;
            const problemId = `${problem.contestId}${problem.index}`;

            // Count total submissions
            res.stats.totalSubmissions++;

            let v = sub.verdict;
            if (v) {
                if (v === 'WRONG_ANSWER') v = 'WA';
                else if (v === 'TIME_LIMIT_EXCEEDED') v = 'TLE';
                else if (v === 'MEMORY_LIMIT_EXCEEDED') v = 'MLE';
                else if (v === 'COMPILATION_ERROR') v = 'CE';
                else if (v === 'RUNTIME_ERROR') v = 'RE';
                else if (v === 'OK') {
                    v = 'AC';
                    totalAC++;
                }
                else if (v === 'PASSED_PRETESTS') v = 'Pretest OK';
                res.verdicts[v] = (res.verdicts[v] || 0) + 1;
            }

            if (sub.author && sub.author.participantType) {
                res.participantType[sub.author.participantType] = (res.participantType[sub.author.participantType] || 0) + 1;
            }

            // Timeline data (monthly)
            const date = new Date(sub.creationTimeSeconds * 1000);
            const monthStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
            res.timeline[monthStr] = (res.timeline[monthStr] || 0) + 1;

            // Contest speed analysis
            if (sub.relativeTimeSeconds && sub.relativeTimeSeconds < 2147483647) {
                const minutes = sub.relativeTimeSeconds / 60;
                let speedCategory;
                if (minutes <= 10) speedCategory = '0-10min';
                else if (minutes <= 30) speedCategory = '10-30min';
                else if (minutes <= 60) speedCategory = '30-60min';
                else if (minutes <= 120) speedCategory = '1-2h';
                else if (minutes <= 240) speedCategory = '2-4h';
                else speedCategory = '>4h';
                res.speedAnalysis[speedCategory] = (res.speedAnalysis[speedCategory] || 0) + 1;
            }

            if (!problemState.has(problemId)) problemState.set(problemId, { ac: false, tries: 0 });
            const pState = problemState.get(problemId);

            if (!pState.ac) {
                pState.tries++;
                if (sub.verdict === 'OK') {
                    pState.ac = true;
                    let tryKey = pState.tries === 1 ? 'try1' : pState.tries === 2 ? 'try2' : pState.tries <= 5 ? 'try3_5' : 'tryMore';
                    res.attempts[tryKey] = (res.attempts[tryKey] || 0) + 1;

                    // Performance data
                    if (problem.rating && sub.timeConsumedMillis !== undefined) {
                        res.performance.push([sub.timeConsumedMillis, problem.rating, problem.name]);
                    }

                    // Memory performance data
                    if (problem.rating && sub.memoryConsumedBytes !== undefined) {
                        const memoryKB = Math.round(sub.memoryConsumedBytes / 1024);
                        res.memoryPerformance.push([memoryKB, problem.rating, problem.name]);
                    }

                    // Points tracking
                    if (problem.points) {
                        res.stats.totalPoints += problem.points;
                    }

                    // Track highest rating
                    if (problem.rating && problem.rating > res.stats.highestRating) {
                        res.stats.highestRating = problem.rating;
                    }

                    solvedProblems.set(problemId, sub);
                    if (res.unsolved[problemId]) delete res.unsolved[problemId];
                } else {
                    res.unsolved[problemId] = { contestId: problem.contestId, problemIndex: problem.index };
                }
            }
        });

        // Process solved problems
        solvedProblems.forEach(sub => {
            const { rating, tags } = sub.problem;
            const lang = sub.programmingLanguage;
            if (rating) res.rating[rating] = (res.rating[rating] || 0) + 1;
            if (lang) res.lang[lang] = (res.lang[lang] || 0) + 1;
            if (tags && tags.length > 0) tags.forEach(tag => { res.tags[tag] = (res.tags[tag] || 0) + 1; });
        });

        // Calculate stats
        res.stats.solvedProblems = solvedProblems.size;
        res.stats.acRate = totalSubmissions > 0 ? (totalAC / totalSubmissions * 100) : 0;

        // Calculate max streak using monthly timeline data
        res.stats.maxStreak = calculateMaxStreakFromTimeline(res.timeline);

        return res;
    }

    function calculateMaxStreakFromTimeline(timelineData) {
        if (Object.keys(timelineData).length === 0) return 0;

        const months = Object.keys(timelineData).sort();
        let maxStreak = 1;
        let currentStreak = 1;

        for (let i = 1; i < months.length; i++) {
            const prevMonth = new Date(months[i - 1] + '-01');
            const currMonth = new Date(months[i] + '-01');

            // Check if months are consecutive
            const diffMonths = (currMonth.getFullYear() - prevMonth.getFullYear()) * 12 +
                              (currMonth.getMonth() - prevMonth.getMonth());

            if (diffMonths === 1) {
                currentStreak++;
                maxStreak = Math.max(maxStreak, currentStreak);
            } else {
                currentStreak = 1;
            }
        }

        return maxStreak;
    }

    // --- 加载状态管理 ---
    const showLoading = () => {
        hideLoading();
        const html = `
            <div id="cf-loading-mask" style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(255,255,255,0.8); z-index: 9999; display: flex; flex-direction: column; justify-content: center; align-items: center;">
                <div id="cf-loading-close" style="position: absolute; top: 20px; right: 30px; font-size: 36px; cursor: pointer; color: #888; font-weight: bold; line-height: 1; transition: color 0.2s;" onmouseover="this.style.color='#333'" onmouseout="this.style.color='#888'" title="Close">&times;</div>
                <div style="width: 50px; height: 50px; border: 5px solid #f3f3f3; border-top: 5px solid #0073e6; border-radius: 50%; animation: cf-spin 1s linear infinite;"></div>
                <h3 id="cf-loading-text" style="margin-top: 20px; color: #333; font-family: sans-serif;">${t('loading')}</h3>
                <style>@keyframes cf-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }</style>
            </div>
        `;
        document.body.insertAdjacentHTML('beforeend', html);

        document.getElementById('cf-loading-close').addEventListener('click', hideLoading);
        document.getElementById('cf-loading-mask').addEventListener('click', (e) => {
            if (e.target.id === 'cf-loading-mask') hideLoading();
        });
    };

    const hideLoading = () => {
        const el = document.getElementById('cf-loading-mask');
        if (el) el.remove();
    };

    // --- 主入口 ---
    async function init() {
        const pathname = window.location.pathname;
        const handle = pathname.substring(pathname.lastIndexOf('/') + 1);
        if (!handle) return;

        showLoading();

        try {
            const response = await fetch(`https://codeforces.com/api/user.status?handle=${handle}`);
            if (!response.ok) throw new Error('API Error');
            const json = await response.json();

            if (json.status === "OK") {
                const processedData = processSubmissions(json.result);
                drawCharts(processedData);
            }
        } catch (err) {
            console.error("Failed to load CF Data:", err);
            alert(t('fetchError'));
        } finally {
            hideLoading();
        }
    }

    init();
})();