我正在参加「掘金·启航计划」
引入D3模块
- 引入整个D3模块。
<!-- D3模块 -->
<script src="https://d3js.org/d3.v7.min.js"></script>
数据
- 在热力图中展示的数据。
const dataArr = {
'2022-01-15': 3,
'2022-05-16': 2,
'2022-05-17': 1,
'2022-05-18': 3,
'2022-05-19': 6,
'2022-05-20': 5,
'2022-07-21': 1,
'2022-05-22': 2,
'2022-05-23': 1,
'2022-03-24': 3,
'2022-05-25': 2,
'2022-05-26': 5,
'2022-05-27': 4,
'2022-05-28': 6,
'2022-05-29': 2,
'2022-05-30': 1,
'2022-06-31': 3
}
添加画布
// 画布
const width = 1200
const height = 240
const margin = 40
const svg = d3
.select('.d3Chart')
.append('svg')
.attr('width', width)
.attr('height', height)
.style('background-color', '#1a3055')
// 图
const chart = svg.append('g').attr('transform', `translate(${margin}, ${margin})`)
比例尺和配置信息
const scaleColor = d3
.scaleLinear()
.domain([0, d3.max(Object.values(dataArr))])
.range(['#FC9', '#F96'])
d3.max()
获取数组中的最大值。
- 创建颜色线性比例尺。
/**
* 数据组装
* */
function generateDataset(options = { fill: {} }) {
// 开始时间
const startDate = options.startDate
? new Date(options.startDate)
: new Date(new Date().getFullYear() + '-' + '01' + '-' + '01')
// 结束时间
const endDate = options.endDate ? new Date(options.endDate) : new Date()
// 相隔天数
const totalDays = Math.floor((endDate.getTime() - startDate.getTime()) / 86400000)
// 循环天数
let year, month
let yearIndex = -1,
monthIndex = -1
let yearGroup = []
let dayTem = 0
while (dayTem <= totalDays) {
const dateName = d3.timeFormat('%Y-%m-%d')(new Date(startDate.getTime() + 86400000 * dayTem))
const dateArr = dateName.split('-')
// 年
if (!year || dateArr[0] !== year) {
year = dateArr[0]
yearGroup.push({
name: dateArr[0],
monthGroup: []
})
yearIndex++
monthIndex = -1
}
// 月
if (!month || dateArr[1] !== month) {
month = dateArr[1]
yearGroup[yearIndex].monthGroup.push({
name: dateArr[0] + '-' + dateArr[1],
dayGroup: []
})
monthIndex++
}
// 获取热力数据值
let total = null
if (options.fill.hasOwnProperty(dateName)) {
total = options.fill[dateName]
}
// 天
yearGroup[yearIndex].monthGroup[monthIndex].dayGroup.push({
name: dateName,
dayTem: dayTem + startDate.getDay(),
total
})
dayTem++
}
return yearGroup
}
// startDate:日历开始时间 endDate:日历结束时间 dataArr:要展示的数据 - 之前定义好的格式
const dayDatas =
generateDataset({ startDate: '2021-11-01', endDate: '2022-9-30', fill: dataArr })
d3.timeFormat('%Y-%m-%d')
转换时间格式。
- 根据传入参数得到,开始时间、结束时间。
- 计算出间隔天数,循环所有天数,归类组装为数组格式数据。
绘制日历块
const yearSvg = chart
.selectAll()
.data(dayDatas)
.enter()
.append('g')
.attr('class', (d) => 'year year-' + d.name)
- 绑定年数据。
const monthSvg = yearSvg
.selectAll()
.data((d) => d.monthGroup)
.enter()
.append('g')
.attr('class', (d) => 'month month-' + d.name)
- 绑定年数据下的,月数据。
// 绘制方块
const daySvg = monthSvg
.selectAll()
.data((d) => d.dayGroup)
.enter()
.append('rect')
.attr('width', 20)
.attr('height', 20)
.attr('rx', 3)
.attr('fill', (d) => {
if (!d.total) {
return '#EFEFEF'
}
return scaleColor(d.total)
})
.attr('x', (d) => Math.floor(d.dayTem / 7) * 21)
.attr('y', (d) => (d.dayTem % 7) * 21)
- 绑定天数据,创建方块元素,计算位置按日历图布局。
绘制月和周
const title = chart.append('g')
// 绘制 周
const weeks = ['日', '一', '二', '三', '四', '五', '六']
title
.append('g')
.attr('class', 'week')
.selectAll('.label')
.data(weeks)
.enter()
.append('text')
.attr('class', 'label')
.attr('x', -25)
.attr('y', 20 / 2)
.attr('dy', (d, i) => i * 21 + 5)
.attr('fill', '#EFEFEF')
.text((d) => d)
- 根据日历图布局,绘制周文本。
let monthAll = []
dayDatas.forEach((element) => {
monthAll = monthAll.concat(element.monthGroup)
})
title
.append('g')
.attr('class', 'month-title')
.selectAll()
.data(monthAll)
.enter()
.append('text')
.attr('x', (d, i) => {
return i * 21 * 4.25 + 40
})
.attr('y', -10)
.attr('fill', '#EFEFEF')
.attr('font-size', '1em')
.attr('font-family', 'monospace')
.text((d) => d.name)
- 获取月数据,在日历图上绘制。
i * 21 * 4.25 + 40
每个月文本的间隔。这里只是简单计算,不是每个月文本都和起始块对齐。
交互
var tooltips = d3
.select('body')
.append('div')
.style('width', 'auto')
.style('height', '40px')
.style('background-color', '#fff')
.style('dispaly', 'flex')
.style('justify-content', 'center')
.style('padding', '10px')
.style('border-radius', '5px')
.style('opacity', 0)
daySvg
.on('mouseenter', (e, d) => {
let message = '--'
if (d.total) {
message = '有 ' + d.total + ' 篇内容'
}
tooltips
.html(d.name + '<br />' + message)
.style('position', 'absolute')
.style('left', `${e.clientX - 60}px`)
.style('top', `${e.clientY - 60}px`)
.style('opacity', 1)
d3.select(d.target).attr('fill', 'red')
})
.on('mouseleave', (e, d) => {
tooltips.style('opacity', 0).style('left', `0px`).style('top', `0px`)
d3.select(d.target).attr('fill', '#EFEFEF')
})
- 创建提示框。
- 获取到所有方块元素,绑定鼠标事件。
- 根据事件修改提示框数据和回复状态。
总结
这里只是简单的介绍了如何绘制日历热力图。还可以在此基础上,进行功能和交互的添加。如对每个月的块边界,进行颜色绘制或在创建块的时候,添加动画一个个的出现。