在vue3后台项目中使用热力图,并给热力图增加点击选中事件
需要实现的效果如上图,主要几个要求点:
1.要求热力图的开始和结束自定义
2.要求热力图的最大值根据接口返回的数据值定义
3.要求初始选中当前日期前一天的日期,同时tooltip 会自动显示昨天的提交次数
4.要求点中那个选中那个日期,同时展示当日的提交次数
使用的组件是echart中的日历图:Examples - Apache ECharts
具体代码:
<template><v-chartref="chartRef":option="chartOption"autoresizetheme="macarons"@click="handleClick"></v-chart>
</template><script setup>
import * as echarts from 'echarts'
import { reactive, watch, ref, nextTick } from 'vue'const props = defineProps({calenderData: {type: Array,required: true}
})const chartRef = ref(null)const chartOption = reactive({tooltip: {formatter: function (p) {return `日期:${p.data[0]}<br/>提交次数:${p.data[1]}`}},visualMap: {min: 0,max: 100, // ✅ 会被动态覆盖type: 'piecewise',orient: 'horizontal',left: 'center',top: 20,text: ['多', '少'],inRange: {color: ['#d4dcf7','#5070dd']}},calendar: {top: 80,left: 30,right: 30,cellSize: ['auto', 13],range: [],itemStyle: { borderWidth: 0.5 },yearLabel: { show: true, color: '#333' },dayLabel: {firstDay: 1,nameMap: ['日', '一', '二', '三', '四', '五', '六']},monthLabel: {nameMap: ['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月']}},series: [{type: 'heatmap',coordinateSystem: 'calendar',data: []},{type: 'scatter',coordinateSystem: 'calendar',symbolSize: 18,itemStyle: {color: 'red',borderColor: '#fff',borderWidth: 2},data: []}]
})function handleClick(params) {if (params.componentType === 'series' && params.seriesType === 'heatmap') {const date = params.data[0]chartOption.series[1].data = [[date]]chartRef.value?.dispatchAction({type: 'showTip',seriesIndex: 0,dataIndex: params.dataIndex})}
}watch(() => props.calenderData,async (newVal) => {if (!newVal || newVal.length === 0) returnconst start = newVal[0].dateconst end = newVal[newVal.length - 1].datechartOption.calendar.range = [start, end]chartOption.series[0].data = newVal.map(item => [item.date, item.count])// ✅ 动态设置 visualMap.maxchartOption.visualMap.max = Math.max(...newVal.map(item => item.count)) || 0// ✅ 默认选中昨天const yesterday = new Date()yesterday.setDate(yesterday.getDate() - 1)const yesterdayStr = yesterday.toISOString().slice(0, 10)const index = newVal.findIndex(item => item.date === yesterdayStr)if (index !== -1) {chartOption.series[1].data = [[yesterdayStr]]await nextTick()setTimeout(() => {chartRef.value?.dispatchAction({type: 'showTip',seriesIndex: 0,dataIndex: index})}, 200)}},{ immediate: true, deep: true }
)
</script>
这里 我的这个组件是需要引入父组件中 所以数据calenderData是由父组件传进来的
基本数据格式是:
{ date: '2025-08-18', count: 5 },{ date: '2025-08-19', count: 12 },{ date: '2025-08-20', count: 3 },{ date: '2025-08-21', count: 20 },
我这个使用的是一年的数据量比较大,就不展示出来了
核心步骤:
-
日历范围设置
chartOption.calendar.range = [start, end]
根据接口返回的第一条数据和最后一条数据,动态生成热力图的范围。
-
热力图数据赋值
chartOption.series[0].data = newVal.map(item => [item.date, item.count])
将
{ date, count }
转换成 ECharts 所需的二维数组[日期, 数值]
。 -
动态最大值设置
chartOption.visualMap.max = Math.max(...newVal.map(item => item.count)) || 0
保证颜色深浅映射合理。
-
默认选中昨天
chartOption.series[1].data = [[yesterdayStr]]
使用一个额外的 scatter 系列 高亮昨天,并通过
dispatchAction
主动触发 tooltip。 -
点击选中功能
在handleClick
中获取点击日期,更新 scatter 高亮点,并用dispatchAction
展示 tooltip。
其实实现这些功能的主要就是热历图中的配置项。
以下是对这段代码的详细注释,有需要的可以查看
<template> <!-- 模板区域:渲染一个基于 ECharts 的图表组件 --><v-chartref="chartRef" <!-- 通过 ref 获取图表实例,用于后续调用 dispatchAction 等 API -->:option="chartOption" <!-- 绑定 ECharts 配置项(响应式对象) -->autoresize <!-- 容器大小变化时自动触发图表自适应 -->theme="macarons" <!-- 使用 ECharts 的 macarons 主题(需已注册/引入) -->@click="handleClick" <!-- 监听图表上的点击事件(ECharts 事件) -->></v-chart>
</template><script setup>
import * as echarts from 'echarts' // 引入 ECharts 主库(本例主要通过 <v-chart> 使用,但类型/工具函数可能会用到)
import { reactive, watch, ref, nextTick } from 'vue' // 从 Vue 引入响应式工具与生命周期工具const props = defineProps({ // 声明组件接收的 props(<script setup> 语法)calenderData: { // 外部传入的日历数据数组type: Array, // 类型为数组:[{ date: 'YYYY-MM-DD', count: number }, ...]required: true // 必须传入,否则组件无法正常渲染}
})const chartRef = ref(null) // 定义对 <v-chart> 组件的引用,用于获得图表实例(chartRef.value)const chartOption = reactive({ // 定义 ECharts 配置,并用 reactive 包裹使其响应式tooltip: { // 提示框配置:悬浮/显示数据详情formatter: function (p) { // 自定义 formatter,p 为数据点参数return `日期:${p.data[0]}<br/>提交次数:${p.data[1]}` // 将 [日期, 数值] 格式化为多行 HTML}},visualMap: { // 视觉映射:用颜色深浅映射数值大小min: 0, // 颜色映射的最小值(固定为 0)max: 100, // ✅ 会被动态覆盖 // 初始最大值先给个默认值,后续根据数据动态覆盖type: 'piecewise', // 分段型 visualMap(更清晰的档位显示)orient: 'horizontal', // 水平布局left: 'center', // 居中放置top: 20, // 距离容器顶部 20pxtext: ['多', '少'], // 文案:右边代表较多,左边代表较少inRange: { // 数值处于 [min, max] 范围内的样式color: ['#d4dcf7','#5070dd'] // 由浅到深的配色(可根据品牌色调整)}},calendar: { // 日历坐标系配置top: 80, // 日历距离容器顶部 80px(给 visualMap 腾位置)left: 30, // 左边距right: 30, // 右边距cellSize: ['auto', 13], // 单元格尺寸:宽自适应,高 13px(可按需求调)range: [], // 显示的日期范围(将由数据动态设置为 [start, end])itemStyle: { borderWidth: 0.5 }, // 每个日期单元格的边框样式yearLabel: { show: true, color: '#333' }, // 年份标签样式dayLabel: { // 星期标签配置firstDay: 1, // 一周起始日:1 代表周一nameMap: ['日', '一', '二', '三', '四', '五', '六'] // 自定义星期名称},monthLabel: { // 月份标签配置nameMap: [ // 自定义月份中文名称'一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月']}},series: [ // 系列(图形)配置:这里使用两个系列叠加{type: 'heatmap', // 第一层:热力图(根据 count 显示颜色深浅)coordinateSystem: 'calendar', // 使用日历坐标系data: [] // 数据格式为 [[date, count], ...](将由 watch 动态赋值)},{type: 'scatter', // 第二层:散点图(用于高亮选中的某一天)coordinateSystem: 'calendar', // 同样使用日历坐标系,与热力图重叠symbolSize: 18, // 高亮点的尺寸itemStyle: { // 高亮点样式color: 'red', // 点的填充色(明显一些)borderColor: '#fff', // 白色描边,增强突出感borderWidth: 2 // 描边宽度},data: [] // 数据格式为 [[date]](单值散点,表示某天)}]
})function handleClick(params) { // 图表点击事件处理函数(来自 @click)if (params.componentType === 'series' && // 仅在点击的是系列图形(排除坐标轴等)params.seriesType === 'heatmap') { // 并且是热力图的格子时生效const date = params.data[0] // 从被点击的数据中取出日期([date, count] 的第 0 项)chartOption.series[1].data = [[date]] // 更新 scatter 系列为当前日期,实现“高亮选中”chartRef.value?.dispatchAction({ // 主动触发 ECharts 的动作(显示 tooltip)type: 'showTip', // 动作类型:显示提示框seriesIndex: 0, // 指向第 0 个系列(heatmap),因为 tooltip 数据在热力图上dataIndex: params.dataIndex // 指定被点击的数据索引,从而展示对应的提交次数})}
}watch( // 监听 calenderData 的变化(含初始化)() => props.calenderData, // 侦听源:父组件传入的数据async (newVal) => { // 回调:当新数据到来时更新图表if (!newVal || newVal.length === 0) return // 容错:无数据则不处理const start = newVal[0].date // 假定数据已按时间升序:第一条为开始日期const end = newVal[newVal.length - 1].date // 最后一条为结束日期chartOption.calendar.range = [start, end] // 设置日历显示范围为 [start, end]chartOption.series[0].data = newVal.map(item => [item.date, item.count]) // 将数据转为 [[date, count]] 供热力图使用// ✅ 动态设置 visualMap.maxchartOption.visualMap.max = Math.max(...newVal.map(item => item.count)) || 0 // 计算 count 最大值,映射到颜色深浅;空数组时回退 0// ✅ 默认选中昨天const yesterday = new Date() // 获取当前本地时间yesterday.setDate(yesterday.getDate() - 1) // 日期回退 1 天(得到“昨天”)const yesterdayStr = yesterday.toISOString().slice(0, 10) // 转为 YYYY-MM-DD(注意:toISOString 基于 UTC,跨时区可能早/晚一天)const index = newVal.findIndex(item => item.date === yesterdayStr) // 在数据中查找昨天对应的索引if (index !== -1) { // 如果数据里确实包含昨天chartOption.series[1].data = [[yesterdayStr]] // 用 scatter 高亮昨天await nextTick() // 等待 DOM & 图表下一次更新完成,确保实例可用setTimeout(() => { // 再稍微延迟,避免立即触发导致 tooltip 不出现chartRef.value?.dispatchAction({ // 主动展示昨天的数据提示type: 'showTip', // 显示 tooltipseriesIndex: 0, // 指向热力图系列dataIndex: index // 指定昨天在数据中的索引})}, 200) // 200ms 的经验延时(可按实际情况微调)}},{ immediate: true, deep: true } // 配置:立即执行一次(用于首屏渲染)+ 深度侦听(数组/对象内部变更)
)
</script>