上一篇: 盒须图
下一篇: 弦图
代码结构和初始化画布的Chart对象介绍,请先看 这里
本图完整的源码地址: 这里
1 图表效果
2 数据
{
"2018-01-01": 2,
"2018-01-02": 1,
"2018-01-03": 2,
"2018-01-04": 2,
"2018-01-05": 4,
"2018-01-06": 3,
"2018-01-07": 6,
"2018-01-08": 4,
"2018-01-09": 1,
"2018-01-10": 4,
"2018-01-11": 3,
"2018-01-12": 5,
"2018-01-13": 0,
"2018-01-14": 0,
"2018-01-15": 0,
"2018-01-16": 2,
"2018-01-17": 1,
"2018-01-18": 3,
"2018-01-19": 6,
"2018-01-20": 5,
"2018-01-21": 1,
"2018-01-22": 2,
"2018-01-23": 1,
"2018-01-24": 3,
"2018-01-25": 2,
"2018-01-26": 5,
"2018-01-27": 4,
"2018-01-28": 6,
"2018-01-29": 2,
"2018-01-30": 1,
"2018-01-31": 3,
"2018-02-01": 1,
"2018-02-02": 0,
"2018-02-03": 3,
"2018-02-04": 6,
"2018-02-05": 1,
"2018-02-06": 1,
"2018-02-07": 3,
"2018-02-08": 3,
"2018-02-09": 2,
"2018-02-10": 5,
"2018-02-11": 1,
"2018-02-12": 0,
"2018-02-13": 3,
"2018-02-14": 2,
"2018-02-15": 3,
"2018-02-16": 4,
"2018-02-17": 5,
"2018-02-18": 6,
"2018-02-19": 0,
"2018-02-20": 1,
"2018-02-21": 2,
"2018-02-22": 1,
"2018-02-23": 3,
"2018-02-24": 5,
"2018-02-25": 4,
"2018-02-26": 2,
"2018-02-27": 1,
"2018-02-28": 2,
"2018-03-01": 3,
"2018-03-02": 2,
"2018-03-03": 5,
"2018-03-04": 6,
"2018-03-05": 0,
"2018-03-06": 1,
"2018-03-07": 6,
"2018-03-08": 1,
"2018-03-09": 4,
"2018-03-10": 5,
"2018-03-11": 4,
"2018-03-12": 2,
"2018-03-13": 0,
"2018-03-14": 2,
"2018-03-15": 3,
"2018-03-16": 2,
"2018-03-17": 4,
"2018-03-18": 3,
"2018-03-19": 1,
"2018-03-20": 3,
"2018-03-21": 2,
"2018-03-22": 4,
"2018-03-23": 1,
"2018-03-24": 4,
"2018-03-25": 3,
"2018-03-26": 6,
"2018-03-27": 2,
"2018-03-28": 2,
"2018-03-29": 1,
"2018-03-30": 3,
"2018-03-31": 2,
"2018-04-01": 3,
"2018-04-02": 2,
"2018-04-03": 5,
"2018-04-04": 6,
"2018-04-05": 0,
"2018-04-06": 1,
"2018-04-07": 6,
"2018-04-08": 1,
"2018-04-09": 4,
"2018-04-10": 5,
"2018-04-11": 4,
"2018-04-12": 2,
"2018-04-13": 0,
"2018-04-14": 2,
"2018-04-15": 3,
"2018-04-16": 2,
"2018-04-17": 4,
"2018-04-18": 3,
"2018-04-19": 1,
"2018-04-20": 3,
"2018-04-21": 2,
"2018-04-22": 4,
"2018-04-23": 1,
"2018-04-24": 4,
"2018-04-25": 3,
"2018-04-26": 6,
"2018-04-27": 2,
"2018-04-28": 2,
"2018-04-29": 1,
"2018-04-30": 3,
"2018-05-01": 2,
"2018-05-02": 1,
"2018-05-03": 2,
"2018-05-04": 2,
"2018-05-05": 4,
"2018-05-06": 3,
"2018-05-07": 6,
"2018-05-08": 4,
"2018-05-09": 1,
"2018-05-10": 4,
"2018-05-11": 3,
"2018-05-12": 5,
"2018-05-13": 0,
"2018-05-14": 0,
"2018-05-15": 0,
"2018-05-16": 2,
"2018-05-17": 1,
"2018-05-18": 3,
"2018-05-19": 6,
"2018-05-20": 5,
"2018-05-21": 1,
"2018-05-22": 2,
"2018-05-23": 1,
"2018-05-24": 3,
"2018-05-25": 2,
"2018-05-26": 5,
"2018-05-27": 4,
"2018-05-28": 6,
"2018-05-29": 2,
"2018-05-30": 1,
"2018-05-31": 3
}
3 关键代码
导入数据
d3.json('./data.json').then(function(data){
....
设置一些样式配置参数,例如单元格大小、颜色等
const config = {
margins: {top: 80, left: 50, bottom: 50, right: 50},
textColor: 'black',
title: '日期热力图',
hoverColor: 'red',
startTime: '2018-01-01',
endTime: '2018-05-31',
cellWidth: 20,
cellHeight: 20,
cellPadding: 1,
cellColor1: 'white',
cellColor2: 'green',
lineColor: 'yellow',
lineWidth: 2
}
初始化一些常量
/* ----------------------------初始化常量------------------------ */
const startTime = new Date(config.startTime);
const endTime = new Date(config.endTime);
const widthOffset = config.cellWidth + config.cellPadding;
const heightOffset = config.cellHeight + config.cellPadding;
定义颜色插值转化函数
/* ----------------------------颜色转换------------------------ */
chart.scaleColor = d3.scaleLinear()
.domain([0, d3.max(Object.values(data))])
.range([config.cellColor1, config.cellColor2]);
渲染矩形,此处首先使用了一个while
循环遍历startTime
和endTime
之间的所有日期,将未充填颜色的矩形绘制出来。随后再根据导入的data数据,填充对应的矩形的颜色
/* ----------------------------渲染矩形------------------------ */
chart.renderRect = function(){
let currentYear, currentMonth;
let yearGroup, monthGroup;
const initDay = startTime.getDay();
let currentDay = initDay;
const totalDays = getTotalDays(startTime, endTime) + initDay;
const mainBody = chart.body()
.append('g')
.attr('class', 'date')
.attr('transform', 'translate(' + 35 + ',' + 50 + ')')
while(currentDay <= totalDays){
let currentDate = getDate(startTime, currentDay).split('-');
if(!currentYear || currentDate[0] !== currentYear){
currentYear = currentDate[0];
yearGroup = mainBody
.append('g')
.attr('class', 'year ' + currentYear);
}
if (!currentMonth || currentDate[1] !== currentMonth){
currentMonth = currentDate[1];
monthGroup = yearGroup.append('g').attr('class', 'month ' + currentMonth);
}
monthGroup
.append('g')
.attr('class', 'g ' + currentDate.join('-'))
.datum(currentDate.join('-'))
.append('rect')
.attr('width', config.cellWidth)
.attr('height', config.cellHeight)
.attr('x', Math.floor(currentDay / 7) * widthOffset)
.attr('y', currentDay % 7 * heightOffset);
currentDay++;
}
d3.selectAll('.g')
.each(function(d){
d3.select(this)
.attr('fill', chart.scaleColor(data[d] || 0))
.datum({time: d, value: data[d] || 0});
});
function getTotalDays(startTime, endTime){
return Math.floor((endTime.getTime() - startTime.getTime()) / 86400000);
}
function getDate(startTime, day){
const date = new Date(startTime.getTime() + 86400000 * (day - initDay));
return d3.timeFormat("%Y-%m-%d")(date);
}
}
接着渲染分割线,找到每月最后一天对应的矩形,然后根据其位置绘制path
路径,这里有两种情况,一种是分割线有拐点,另一种为一条竖直的线。
/* ----------------------------渲染分隔线------------------------ */
chart.renderLine = function(){
const initDay = startTime.getDay();
const days = [initDay-1];
const linePaths = getLinePath();
d3.select('.date')
.append('g')
.attr('class', 'lines')
.selectAll('path')
.data(linePaths)
.enter()
.append('path')
.attr('stroke', config.lineColor)
.attr('stroke-width', config.lineWidth)
.attr('fill', 'none')
.attr('d', (d) => d);
function getLinePath(){
const paths = [];
d3.selectAll('.month')
.each(function(d,i){
days[i+1] = days[i] + this.childNodes.length;
});
days.forEach((day,i) => {
let path = 'M';
let weekDay = day < 0 ? 6 : day % 7;
if (weekDay !== 6) {
path += Math.floor(day / 7) * widthOffset + ' ' + 7 * heightOffset;
path += ' l' + '0' + ' ' + (weekDay - 6) * heightOffset;
path += ' l' + widthOffset + ' ' + '0';
path += ' l' + '0' + ' ' + (-weekDay - 1) * heightOffset;
} else {
path += (Math.floor(day / 7) + 1) * widthOffset + ' ' + 7 * heightOffset;
path += ' l' + '0' + ' ' + (-7) * heightOffset;
}
paths.push(path);
});
return paths;
}
}
然后渲染文本标签,注意月份标签的间距约为4.25倍单元格大小(一周大约有4.25周)
/* ----------------------------渲染文本标签------------------------ */
chart.renderText = function(){
let week = ['Sun', 'Mon', 'Tue', 'Wed', 'Tur', 'Fri', 'Sat'];
d3.select('.year')
.append('g')
.attr('class', 'week')
.selectAll('.label')
.data(week)
.enter()
.append('text')
.attr('class', 'label')
.attr('x', -40)
.attr('y', heightOffset/2)
.attr('dy', (d,i) => i * heightOffset + 4)
.text((d)=>d);
let months = d3.timeMonth.range(new Date(startTime.getFullYear(), startTime.getMonth(), startTime.getDate()), new Date(endTime.getFullYear(), endTime.getMonth(), endTime.getDate()));
months = months.map((d) => d3.timeFormat("%b")(d));
d3.select('.year')
.append('g')
.attr('class', 'month-label')
.selectAll('text')
.data(months)
.enter()
.append('text')
.attr('x', (d,i) => i*widthOffset*4.25 + widthOffset*2)
.attr('y', -10)
.text((d) => d)
}
最后绑定鼠标交互事件,悬停在矩形上时显示对应日期
/* ----------------------------绑定鼠标交互事件------------------------ */
chart.addMouseOn = function(){
//防抖函数
function debounce(fn, time){
let timeId = null;
return function(){
const context = this;
const event = d3.event;
timeId && clearTimeout(timeId)
timeId = setTimeout(function(){
d3.event = event;
fn.apply(context, arguments);
}, time);
}
}
d3.selectAll('.g')
.on('mouseenter', function(d){
const e = d3.event;
const position = d3.mouse(chart.svg().node());
d3.select(e.target)
.attr('fill', config.hoverColor);
chart.svg()
.append('text')
.classed('tip', true)
.attr('x', position[0]+5)
.attr('y', position[1])
.attr('fill', config.textColor)
.text(d.time);
})
.on('mouseleave', function(d){
const e = d3.event;
d3.select(e.target)
.attr('fill', chart.scaleColor(d.value));
d3.select('.tip').remove();
})
.on('mousemove', debounce(function(){
const position = d3.mouse(chart.svg().node());
d3.select('.tip')
.attr('x', position[0]+5)
.attr('y', position[1]-5);
}, 6)
);
}