日历热力图,月度数据可视化图表(日活跃图、格子图)vue组件

日历热力图,月度数据可视化图表,vue组件

先看效果👇


在线体验https://www.guetzjb.cn/calanderViewGraph/

日历图简单划分为近一年时间,开始时间是 上一年的今天,例如2024/01/01 ------ 2025/01/01,跨度刚好一年,依次从上到下看一排真好七个小方格,分别对应着 周日、周一、周二、周三、周四、周五、周六。

PC端、移动端支持良好

方法颜色支持自定义,可根据数据大小规定颜色深度。

实现方式简单易懂~用到element plus和moment,用前请安装到项目

xml 复制代码
yarn add element-plus moment
# or
npm i element-plus moment

show code

calanderViewGraph.vue

html 复制代码
<script setup lang='ts'>
import moment from 'moment'
import 'moment/dist/locale/zh-cn'

moment.locale('zh-cn')

const props = withDefaults(defineProps<{
  size: number
}>(), {
  size: 10
})

const ansRes: any = []
const date = new Date()
const today = {
  year: date.getFullYear(),
  month: date.getMonth(),
  day: date.getDate()
}
let endTime = moment(date, 'YYYY/MM/DD').format('L')
let startTime = moment((today.year - 1) + '/' + (today.month + 1) + '/' + today.day, 'YYYY/MM/DD').format('L')
const visibleList = ref<Record<string, boolean>>({})
let days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

function isLeapYear(year: number) {
  return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}

function init() {
  //上一年开始遍历
  //下标从0开始
  for (let month = today.month; month <= 12 + today.month; month++) {
    let year = Math.floor(month / 12); //0->前一年 1->今年
    let m = month % 12;
    let remainDay
    let week
    if (month != today.month && month != 12 + today.month) {
      remainDay = days[m] + (isLeapYear(today.year - (year == 0 ? 1 : 0)) && m == 1 ? 1 : 0) // 闰年补1
      week = new Date(today.year - (year == 0 ? 1 : 0) + '/' + (m + 1) + '/1').getDay()
    } else {
      if (month == today.month) {
        remainDay = days[m] - today.day + 1 + (isLeapYear(today.year - (year == 0 ? 1 : 0)) && m == 1 ? 1 : 0) // 例如 1/1 ~ 1/2 日期相差2天,相减+1
        week = new Date(today.year - (year == 0 ? 1 : 0) + '/' + (m + 1) + '/' + today.day).getDay()
      } else {
        remainDay = today.day + (isLeapYear(today.year - (year == 0 ? 1 : 0)) && m == 1 ? 1 : 0)
        week = new Date(today.year - (year == 0 ? 1 : 0) + '/' + (m + 1) + '/1').getDay()
      }
    }

    ansRes.push({
      year,
      month: m,
      remainDay,
      week,
      rev: month == today.month // 第一个日期必须反转 例如2024/1/20 剩余11天,应该显示2024/1/20 ~ 2024/1/31
    })
  }
}

const viewsList = ref<any>({
  totalCnt: 0,
  views: {
    // '2025/01/20': 10 //格式 ------ (key:日期,value:数量)
  },
  colors: {
    // '2025/01/20': '#fff000' //格式 ------ (key:日期,value:色值)
  }
})

// 自定义颜色
const colorArr = ["#E0F8E0", "#C6E0C6", "#AEE6AE", "#96EA96", "#7EF07E", "#66F566", "#4EF94E", "#36FD36", "#1EFF1E", "#00CC00"]
function getColorFunc(value: number): string {
  let i = 0
  while (value > 10 && i < colorArr.length) {
    value -= 10
    i += 1
  }
  return colorArr[i]
}

function generateData() {
  let startTimeStamp = new Date(startTime).getTime()
  let endTimeStamp = new Date(endTime).getTime()
  // 随机生成365个数据
  for (let i = 0; i < 365; i++) {
    let randomTimeStamp: number = (endTimeStamp - Math.random() * (endTimeStamp - startTimeStamp)) //  随机减一个随机时间戳,相当于在今天的时间戳基础上减
    let dateStr: string = moment(randomTimeStamp).format('YYYY/MM/DD')
    if (!viewsList.value.views[dateStr]) {
      viewsList.value.views[dateStr] = 0
    }
    let curCnt = Math.random() * 100 | 0 // |0去除小数点 
    viewsList.value.views[dateStr] += curCnt
    viewsList.value.totalCnt += curCnt
    viewsList.value.colors[dateStr] = getColorFunc(viewsList.value.views[dateStr])
  }
}

const formatDate = (year: number, month: number, day: number) => {
  return moment(today.year + (year == 0 ? -1 : 0) + '/' + (month + 1) + '/' + (day + 1), 'YYYY/MM/DD').format('L')
}

init()
onMounted(() => {
  generateData()
})

</script>

<template>
  <div class="calander_box">
    <p class="view_title">
      近一年共浏览
      <span style="font-weight: bold;padding: 0 5px;">{{ viewsList?.totalCnt != null ?
        viewsList?.totalCnt : '...' }}
      </span>
      次
    </p>
    <el-scrollbar>
      <div class="mobile_wrap">
        <div class="calander_view_g_wrap">
          <div class="views_wrap" v-for="month in ansRes" v-show="month.remainDay > 0">
            <!-- 一排 7个 加边距(20px) -->
            <div class="views_month" :style="{ height: props.size * 7 + 20 + 'px' }">
              <!-- 伪装的格子 -->
              <div class="views_day" :style="{
                width: props.size + 'px',
                height: props.size + 'px'
              }" v-for="_offset in month.week" style="background: transparent;cursor: auto;">
              </div>
              <!-- 真正显示的格子 -->
              <div v-for="(_day, index) in month.remainDay">
                <el-tooltip effect="dark" :visible="visibleList[formatDate(month.year, month.month, index)]"
                  :content="`${formatDate(month.year, month.month, !month.rev ? index : (days[month.month] - (month.remainDay - index)))}  ${viewsList?.views[formatDate(month.year, month.month, index)] || 0}次浏览`"
                  placement="top-start">
                  <div class="views_day" @mouseenter="visibleList[formatDate(month.year, month.month, index)] = true"
                    @mouseleave="visibleList[formatDate(month.year, month.month, index)] = false" :style="{
                      background: viewsList?.colors[formatDate(month.year, month.month, index)],
                      width: props.size + 'px',
                      height: props.size + 'px'
                    }">
                  </div>
                </el-tooltip>
              </div>
            </div>
            <p style="color: #a2a2a2;">{{ month.month + 1 + '月' }}</p>
          </div>
        </div>
      </div>
    </el-scrollbar>
  </div>
</template>

<style lang='scss' scoped>
.calander_box {
  width: 100%;
  padding: 20px;

  .view_title {
    font-size: 18px;
    padding-left: 10px;
    margin-bottom: 20px;
  }

  .mobile_wrap {
    width: fit-content;

    @media screen and (max-width:480px) {
      width: 800px;
      white-space: nowrap;
      overflow-anchor: auto;
    }

    .calander_view_g_wrap {
      display: flex;
      justify-content: space-between;

      .views_wrap {
        width: 100%;
        margin-right: 8px;
        margin-left: 8px;

        p {
          text-align: center;
          margin-top: 10px;
        }

        .views_month {
          width: calc(100% / 12);
          height: 90px;
          display: flex;
          flex-direction: column;
          flex-wrap: wrap;

          @media screen and (max-width:1200px) {
            height: 50px;
          }

          .views_day {
            margin: 0 2px 2px 0;
            border-radius: 2px;
            background: #F7F7F8;
            cursor: pointer;

            @media screen and (max-width:1200px) {
              width: 5px;
              height: 5px;
            }
          }
        }
      }
    }
  }

}
</style>

使用方法:

传入size表示方格的宽度和高度,

如果不想要方形,可以自己改样式实现(注意调整外部div高度,必须一排七个,否则周(日、一......六)的顺序会错乱)

html 复制代码
<calanderViewGraph :size="10"/>
相关推荐
大嘴吧Lucy几秒前
大模型 | AI驱动的数据分析:利用自然语言实现数据查询到可视化呈现
人工智能·信息可视化·数据分析
热忱112829 分钟前
elementUI Table组件实现表头吸顶效果
前端·vue.js·elementui
林涧泣37 分钟前
【Uniapp-Vue3】setTabBar设置TabBar和下拉刷新API
前端
Rhys..43 分钟前
Jenkins pipline怎么设置定时跑脚本
运维·前端·jenkins
易林示1 小时前
chrome小插件:长图片等分切割
前端·chrome
大叔_爱编程1 小时前
wx035基于springboot+vue+uniapp的校园二手交易小程序
vue.js·spring boot·小程序·uni-app·毕业设计·源码·课程设计
zhaocarbon1 小时前
VUE elTree 无子级 隐藏展开图标
前端·javascript·vue.js
浏览器爱好者2 小时前
如何在AWS上部署一个Web应用?
前端·云计算·aws
Elastic 中国社区官方博客2 小时前
设计新的 Kibana 仪表板布局以支持可折叠部分等
大数据·数据库·elasticsearch·搜索引擎·信息可视化·全文检索·kibana
xiao-xiang2 小时前
jenkins-通过api获取所有job及最新build信息
前端·servlet·jenkins