热力图组件搭建

556 阅读3分钟

目标

leetcode和github都有活跃度的热力图,本文的目标就是搭建一个类似leetcode的热力图组件

格子布局

为简化组件复杂度,将热力图组件中月份单独拆分为子组件。

渲染上需要考虑以下问题

问题一,本月有多少天,决定渲染多少个格子

使用dayjs,获取当前月month最后一天的日期,这样就能够获得本月的天数了

const days = dayjs(month).endOf('month');
const dayCount = days.date();

问题二,本月第一天是星期几,从哪一个格子开始渲染

使用dayjs获取当前月份月初是星期几

// 周日会返回0
const startDayOfWeek = dayjs(month).startOf('month').day() || 7;

问题三,从上到下,从左到右的布局,以及怎么指定开始的格子位置

  1. 使用grid布局,指定行数=7,指定元素流动方向为从上到下
  2. 当前为星期四,就用三个空白格子占位

格子颜色

问题四,由浅入深的格子颜色怎么显示

每一个格子的渲染时都会接收以下数据,level表示程度

export interface HeatMapData {
    level: number;
    duration: number;
    record: { category: string; content: string }[];
}

// key是日期,值是热力图每一格的数据
export type LogData = Record<string, HeatMapData>;

指定5个level,由浅入深的颜色,渲染时根据level动态为每个格子增加对应的class

.color-level-0 { 
    background-color: #ebedf0;
}

.color-level-1 {
    background-color: #ace7ae;
}

.color-level-2 {
    background-color: #69c16e;
}

.color-level-3 {
    background-color: #549f57;
}

.color-level-4 {
    background-color: #386c3e;
}

鼠标悬浮展示内容

根据每个格子接收到的数据,拼接而成,比较简单

export interface HeatMapData {
    level: number;
    duration: number;
    record: { category: string; content: string }[];
}

// key是日期,值是热力图每一格的数据
export type LogData = Record<string, HeatMapData>;

代码

热力图组件代码

component/heat-map/index.ts,约定类型

export interface HeatMapData {
    level: number;
    duration: number;
    record: { category: string; content: string }[];
}

// key是日期,值是热力图每一格的数据
export type LogData = Record<string, HeatMapData>;

component/heat-map/month.vue,渲染当前月份格子

<template>
    <div class="flex flex-col gap-3">
        <div class="inline-grid gap-1 grid-rows-7 grid-flow-col">
            <div v-for="item in emptyGridList" :key="item" />
            <template v-for="item in gridCount" :key="item">
                <el-tooltip v-if="logData[item]" effect="light">
                    <template #content>
                        <div class="text-3.5">
                            <div>{{ `${item},${logData[item].duration}min` }}</div>
                            <div v-for="recordItem in logData[item].record" :key="recordItem.category">
                                {{ `${recordItem.category} : ${recordItem.content}` }}
                            </div>
                        </div>
                    </template>
                    <div class="rd-1 w-4 h-4 gap-1" :class="getBgClass(item)" />
                </el-tooltip>
                <div v-else class="rd-1 w-4 h-4 gap-1 color-level-0" />
            </template>
        </div>
        <div>{{ month }}月</div>
    </div>
</template>

<script setup lang="ts">
import { dayjs } from 'element-plus';
import type { LogData } from '.';

const { month } = defineProps<{
    month: string;
}>();

// 本月有几天
const days = dayjs(month).endOf('month');
const dayCount = days.date();
const gridCount = Array.from({ length: dayCount }, (item, index) => {
    const day = String(index + 1).padStart(2, 0);
    const date = `${month}-${day}`;
    return date;
});

const logData = inject('logData') as LogData;

// 本月第一天是周几
const startDayOfWeek = dayjs(month).startOf('month').day() || 7;
const emptyGridList = Array.from({ length: startDayOfWeek - 1 }, (item, index) => `empty-${index}`);

function getBgClass(date: string) {
    const level = logData[date]?.level;
    return `color-level-${level}`;
}
</script>

<style scoped>
.color-level-0 {
    background-color: #ebedf0;
}

.color-level-1 {
    background-color: #ace7ae;
}

.color-level-2 {
    background-color: #69c16e;
}

.color-level-3 {
    background-color: #549f57;
}

.color-level-4 {
    background-color: #386c3e;
}
</style>

component/heat-map/index.vue,渲染本年的所有格子

<template>
    <div class="flex text-3.5 mt-10">
        <div class="mr-4">
            <div v-for="item in daysInWeek" :key="item">
                {{ item }}
            </div>
        </div>
        <div class="flex gap-4">
            <Month v-for="item in monthList" :key="item.key" :month="item.month" />
        </div>
    </div>
</template>

<script setup lang="ts">
import Month from './month.vue';

const props = defineProps<{
    year: string;
}>();

const daysInWeek = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];

const monthList = computed(() => {
    return Array.from({ length: 12 }, (item, index) => {
        const monthStr = `${index + 1}`.padStart(2, 0);
        return {
            key: `${props.year}-month-${index}`,
            month: `${props.year}-${monthStr}`,
        };
    });
});
</script>

<style scoped>

</style>

页面中使用

<template>
  <HeatMap  :year="year" :log-data="logData" />
</template>

logData格式如下:

export interface HeatMapData {
    level: number;
    duration: number;
    record: { category: string; content: string }[];
}

// key是日期,值是热力图每一格的数据
export type LogData = Record<string, HeatMapData>;

效果

完成搭建!