d3实现热力图

963 阅读3分钟

我想实现的热力图的样式时这样的:

我的数据结构为:

const data ={
   '2021.06': [
     { date: '2021-06-01',value: 20},
     { date: '2021-06-02',value: 20}
     ......
    ]
   ......
}

它的html结构是这样的:

其中,svg中包含左侧的星期几的标识,包含每一月rect的组g元素,class为date-list为热力图下面的时间。

这个图的实现可以分为三个部分:

  • 左侧星期的显示。
  • 热力图的显示
  • 热力图下方的时间的显示。

第一点和第三点都比较好实现,就不说了,重点说一下第二点:热力图的显示。

热力图的显示也可以分为三点:

  • 一个月的rect的组g的显示位置。

  • 一个g中的每一个rect的显示位置。

  • rect的填色。

具体的实现过程如下:

1、处理数据

将数据处理成这样:

const list = [
     ['2021.06', [
       { date: '2021-06-01',value: 20},
       { date: '2021-06-02',value: 20}, 
         ......
       ]
     ......
    ]

2、组g位置的显示

  • 添加svg元素

        关于svg的宽度和高度,我们可以通过react-container-dimensions包来获取,设为svg的width和height,id为attack-protrait-heat-map的元素就是我们自己创建的svg元素。

const svg = d3.select(`#attack-protrait-heat-map`).attr('width', width).attr('height', height);
  • 添加左侧星期显示

    svg.append('g')
    .attr('transform', 'translate(0, 10)')      
    .selectAll('text')      
    .data(d3.range(7))      
    .join('text')      
    .attr('x', 0)      
    .attr('y', i => (i + 0.5) * cellSize)      
    .attr('dy', '0.32em')      
    .attr('font-size', '12px')      
    .attr('fill', '#999')      
    .text(i => '一二三四五六日'[i]);
    

这一步是通过向svg中添加一个g用来分组,并向g中添加7个text,每个text的x值时相同的,y值为(i + 0.5) * cellSize,cellSize为热力图小方块的大小。

  • 添加g元素

    const yearsEle = svg
    .selectAll('g')
    .data(years)
    .join('g')
    .attr('class', (d, i) => year-item year-item-${i}) .attr('len', d => d?.[1]?.length)
    .attr('index', (d, i) => i);

这里向svg中添加g元素,每一个g都是用来放一个月的rect元素的。index是为了之后定位每一个rect的位置。

  • 向每一个g元素中添加一个月的rect元素(添加小方块)

    yearsEle
      .selectAll('.year-item')  // 选中所有的class为year-item的g元素
      .append('rect')  // 向g元素中添加rect元素
      .data(([date, values]) => { 
        svg.selectAll('.month-item').remove();  // 为了防止每次渲染都会添加rect元素,造成rect元素要来越多,所以先移除所有的rect
    
        // date为list中 2021.06等月份
        // values为数据,如:[{date: '2021-06-01', value: 20}, {date: '2021-06-02', value: 20}]
        return values; 
      })
      .join('rect')
      .attr('class', 'month-item')
      .attr('width', cellSize)
      .attr('height', cellSize)
      .attr('x', function (d) {
    
       // 每一个小方块x值的计算过程:
       //    获取到父元素g的index值。
       //    date.getDay()获取到date为一周的第几天,为0~6,我们要实现的小方块rect从上到下排列顺序为从周一开始
       //    通过date.getDay() === 0 ? 7 : date.getDay()获取到1~7的值
       //    7-day获取到date还差几天这周结束。
       //    new Date(date).getDate() + (7 - day) 思路看补充1
       //    Math.ceil(total / 7)获取到rect应该为这个月的第几周,也就是在图中这一月的第几列
       //    第几周 * rect的宽度 + 父元素g的偏移量(index*110)+30,也就是rect的x位置
    
        const date = new Date(d.date.replace(/-/g, '/')); 
        const index = Number(this.parentNode.getAttribute('index'));
    
        // 如果想要从上到下从周日开始,就不用这一步处理,只要将侧边显示周几改为周日开始
        const day = date.getDay() === 0 ? 7 : date.getDay();
        const total = new Date(date).getDate() + (7 - day);
    
        return Math.ceil(total / 7) * cellSize + index * 110 + 30;
      })
      .attr('y', d => {
    
        // 每一个小方块y值计算的过程:
        // 获取到date为这一周的第几天,再乘以小方块的宽度即可。
        const date = new Date(d.date.replace(/-/g, '/'));
        const day = date.getDay() === 0 ? 7 : date.getDay();
        return day * cellSize - 5;
      })
      .attr('fill', (d = {}) => {
        // 为小方块添加颜色
        const { value = 0, compensatory } = d;
        return compensatory ? '#f7f7f7' : colorList[value];
      })