D3 二维图表的绘制系列(二十七)日历热力图

上一篇: 盒须图

下一篇: 弦图

代码结构和初始化画布的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循环遍历startTimeendTime之间的所有日期,将未充填颜色的矩形绘制出来。随后再根据导入的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)
            );
    }

大功告成!!!


如果觉得这篇文章帮助了您,请打赏一个小红包鼓励作者继续创作哦!!!

在这里插入图片描述

  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值