【网站建设】”热力图“功能实现

实现下面这种类似 GitHub 贡献热力图的「创作指数日历」

修改子主题functions.php代码

<?php
// 1. 数据获取:当前自然年文章
function contribution_heatmap_get_data() {
    // 统一使用WordPress时区获取年份
    $current_year = wp_date('Y');
    
    // 优化WP_Query参数:解决查询遗漏+边界模糊问题
    $args = array(
        'post_type' => 'post',
        'post_status' => 'publish',
        'posts_per_page' => -1,
        'fields' => 'post_date',
        'no_found_rows' => true,
        'ignore_sticky_posts' => true,
        'date_query' => array(
            array(
                'column' => 'post_date',
                'after' => $current_year . '-01-01 00:00:00',
                'before' => $current_year . '-12-31 23:59:59',
                'inclusive' => true,
            ),
        ),
    );
    
    // 执行查询
    $query = new WP_Query($args);
    $contribution_data = array();

    // 日志:打印查询总数
    error_log('=== 热力图统计开始 ===');
    error_log('当前统计年份:' . $current_year);
    error_log('本次查询到文章总数:' . count($query->posts));

    foreach ($query->posts as $post) {
        // 1. 原始日期(数据库里的UTC时区datetime字符串)
        $raw_post_date = $post->post_date;
        
        // 2. 关键:直接提取前10位(YYYY-MM-DD),跳过时区转换,避免跨天
        if (strlen($raw_post_date) < 10) {
            error_log('警告:原始日期 ' . $raw_post_date . ' 格式异常,已跳过');
            continue;
        }
        $formatted_date = substr($raw_post_date, 0, 10);
        
        // 3. 日志:打印原始日期、提取后的统计日期(方便验证)
        error_log('原始UTC日期:' . $raw_post_date . ' | 统计日期:' . $formatted_date);

        // 4. 统计每日文章数(无数量上限)
        if (isset($contribution_data[$formatted_date])) {
            $contribution_data[$formatted_date]++;
        } else {
            $contribution_data[$formatted_date] = 1;
        }
    }

    // 日志:打印最终统计结果
    error_log('最终日期统计结果:' . print_r($contribution_data, true));
    error_log('=== 热力图统计结束 ===' . "\n");

    wp_reset_postdata();
    return $contribution_data;
}

// 2. 短代码:新增鼠标悬浮tooltip,区分过去/未来日期
function contribution_heatmap_shortcode() {
    ob_start();
    $contribution_data = contribution_heatmap_get_data();
    $json_data = json_encode($contribution_data);
    $current_year = date('Y');
    ?>

    <div id="contribution-wrapper" style="width: 800px; margin: 20px 0;">
        <!-- 右上角4级色块图例 -->
        <div id="heatmap-legend" style="display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin-bottom: 10px;">
            <span style="font-size: 0.9rem; color: #666;">不活跃</span>
            <div style="width: 20px; height: 20px; background: #ffffff; border: 1px solid #e0e0e0;"></div>
            <div style="width: 20px; height: 20px; background: #c8e6c9; border: 1px solid #e0e0e0;"></div>
            <div style="width: 20px; height: 20px; background: #66bb6a; border: 1px solid #e0e0e0;"></div>
            <div style="width: 20px; height: 20px; background: #2e7d32; border: 1px solid #e0e0e0;"></div>
            <span style="font-size: 0.9rem; color: #666;">活跃</span>
        </div>

        <!-- 热力图容器 -->
        <div id="contribution-heatmap" style="width: 100%; height: 250px; padding: 20px; border: 1px solid #ddd;">
            <div id="full-calendar" style="width: 100%; height: 100%;"></div>
        </div>
    </div>

    <script src="https://cdn.bootcdn.net/ajax/libs/d3/7.8.5/d3.min.js"></script>
    <script>
    window.onload = function() {
        if (!d3) {
            document.getElementById("contribution-heatmap").innerHTML = "D3加载失败";
            return;
        }

        const contributionData = <?php echo $json_data; ?>;
        const startDate = new Date("<?php echo $current_year; ?>", 0, 1);
        const endDate = new Date("<?php echo $current_year; ?>", 11, 31);
        const today = new Date(); // 获取当前日期(用于区分过去/未来)
        today.setHours(0, 0, 0, 0); // 重置时间为0点,避免时分秒干扰日期判断
        const dateRange = d3.timeDays(startDate, endDate);
        const cellSize = 14;
        const margin = { top: 20, right: 10, bottom: 30, left: 10 };

        // ******** 新增1:创建tooltip元素(默认隐藏) ********
        const tooltip = d3.select("body").append("div")
            .attr("class", "heatmap-tooltip")
            .style("position", "absolute")
            .style("background-color", "#ffffff")
            .style("border", "1px solid #e0e0e0")
            .style("border-radius", "4px")
            .style("padding", "6px 10px")
            .style("font-size", "0.9rem")
            .style("box-shadow", "0 2px 5px rgba(0, 0, 0, 0.1)")
            .style("opacity", 0) // 默认透明隐藏
            .style("pointer-events", "none") // 不阻挡鼠标事件
            .style("z-index", "9999"); // 确保在最上层

        // 创建SVG容器
        const svg = d3.select("#full-calendar")
            .append("svg")
            .attr("width", "100%")
            .attr("height", "100%")
            .append("g")
            .attr("transform", `translate(${margin.left}, ${margin.top})`);

        // ******** 新增2:日期格式化函数(2026-01-26 Mon) ********
        const dateFormatter = d3.timeFormat("%Y-%m-%d %a"); // %Y=年, %m=月, %d=日, %a=星期缩写(Mon/Tue等)

        // 热力图颜色:与4级图例对应
        const cells = svg.selectAll(".cell")
            .data(dateRange)
            .enter().append("rect")
            .attr("class", "cell")
            .attr("width", cellSize - 1)
            .attr("height", cellSize - 1)
            .attr("x", d => d3.timeWeek.count(d3.timeYear(d), d) * cellSize)
            .attr("y", d => d3.timeFormat("%w")(d) * cellSize)
            .attr("fill", d => {
                const dateStr = d3.timeFormat("%Y-%m-%d")(d);
                if (contributionData[dateStr] >= 3) return "#2e7d32"; // 等级4(活跃)
                if (contributionData[dateStr] >= 2) return "#66bb6a"; // 等级3
                if (contributionData[dateStr] >= 1) return "#c8e6c9"; // 等级2
                return "#ffffff"; // 等级1(不活跃)
            })
            .attr("stroke", "#f0f0f0");

        // ******** 新增3:鼠标悬浮事件(区分过去/未来日期) ********
        cells
            // 鼠标移入:显示tooltip
            .on("mouseover", function(event, d) {
                // 1. 格式化日期为「2026-01-26 Mon」格式
                const formattedDate = dateFormatter(d);
                // 2. 重置当前日期的时分秒(避免时分秒干扰判断)
                const cellDate = new Date(d);
                cellDate.setHours(0, 0, 0, 0);

                // 3. 区分「过去/当前日期」和「未来日期」
                let tooltipContent = "";
                if (cellDate <= today) {
                    // 过去/当前日期:显示「日期 + 文章数」
                    const dateStr = d3.timeFormat("%Y-%m-%d")(d);
                    const articleCount = contributionData[dateStr] || 0;
                    tooltipContent = `${formattedDate}<br>创作文章:${articleCount} 篇`;
                } else {
                    // 未来日期:只显示「日期」
                    tooltipContent = formattedDate;
                }

                // 4. 显示tooltip并调整位置(跟随鼠标)
                tooltip.html(tooltipContent)
                    .style("left", (event.pageX + 10) + "px") // 鼠标右侧10px
                    .style("top", (event.pageY - 20) + "px") // 鼠标上方20px
                    .transition() // 淡入效果
                    .duration(200)
                    .style("opacity", 0.95);
            })
            // 鼠标移出:隐藏tooltip
            .on("mouseout", function() {
                tooltip.transition() // 淡出效果
                    .duration(500)
                    .style("opacity", 0);
            });

        // 月份标签
        const months = d3.timeMonths(startDate, endDate);
        svg.selectAll(".month-label")
            .data(months)
            .enter().append("text")
            .attr("class", "month-label")
            .attr("x", d => d3.timeWeek.count(d3.timeYear(d), d) * cellSize)
            .attr("y", 7 * cellSize + 20)
            .attr("font-size", "11px")
            .attr("fill", "#666")
            .text(d => d3.timeFormat("%b")(d));
    };
    </script>

    <style>
    .cell:hover {
        stroke: #333 !important;
        stroke-width: 1px !important;
    }
    </style>
    <?php
    return ob_get_clean();
}
add_shortcode('contribution_heatmap', 'contribution_heatmap_shortcode');
?>

验证效果是否生效

  1. 新建 / 编辑页面,在编辑器中输入短代码
[contribution_heatmap]
  1. 点击「发布 / 更新」,前台访问页面(按Ctrl+F5强制清除缓存)。
  2. 此时能看到:
    • 一个完整的 GitHub 风格日历热力图,横向排列月份,纵向排列星期。
    • 有文章的日期显示对应绿色(文章数越多颜色越深),无文章的日期显示白色。
    • 鼠标悬浮在方块上,会显示具体日期和创作文章数。
    • 右侧有活跃度颜色梯度说明,布局整齐美观。
不活跃
活跃

发表回复

Your email address will not be published. Required fields are marked *.

*
*