D3.js学习笔记:绘制热点图

目录

介绍

热力图用矩阵表示数据关系,用颜色表示数值。例如:
Day / Hour Heatmap

显示同时在线的用户数量

上图受下面项目的启发而绘制。

House Hunting All Day, Every Day


该项目使用d3.js,但没有全部使用svg,网格用div绘制。第一个项目中,作者则完全使用svg绘图。所以我主要参照第一个项目实现热力图。

需求

显示每个项目每小时的数据。用颜色表示数据大小。横坐标为小时,纵坐标为项目。

实现

输入数据

图形为矩阵形式,可以考虑用矩阵作为输入数据。但考虑到以下两点:

  1. 每行都是同一事物在不同时间段的数据,应该将这些数据放到一起便于交互查询。
  2. d3的数据机制最适合处理数组,比矩阵更简单直观。

输入数据形如二维数组形式(还有其他附属变量),在数据处理阶段时,将二维数组变为一维数组。 api返回数据:

{
  "row": [
    {
      "values": [
        { "key": 0, "value": 645 },
        { "key": 1, "value": 567 }
		/* ... */
      ]
    },
    {
      "values": [
        { "key": 0, "value": 355 },
        { "key": 2, "value": 4 },
        /* ... */
	  ]
	}
}

与上一篇博文类似,需要将数据扩展到0-24小时范围上。具体方法不再描述,详情请参看上一篇博文。

数据处理

将二维数组转变为一维数据

var heat_map_data = new_value;
var grid_data = [];
var row_name = [];

heat_map_data.row.forEach(function(row_element, row_index, row_array){
    var row = row_element;
    row.chart_values.forEach(function(element, index, array){
        grid_data.push({
            row: row_index,
            key: element.key,
            value: element.value
        })
    });
    row_name.push(row.name);
});

矩阵网格

横坐标

横坐标是小时

var time_count=24;
var time_range = d3.range(0, time_count);
var x_scale = d3.scale.linear().domain([0,time_count]).range([0, grid_svg_attr.width]);

纵坐标

纵坐标是每个项目,不需要单独设置。

颜色

与上一篇博文同样,使用d3.scale.quantile

var max_value = d3.max(grid_data, function(d){return d.value; });
var color_range = colorbrewer.YlOrRd[6];
var color_scale = d3.scale.quantile().domain([0, max_value]).range(color_range);

绘制

var heat_map_rect = grid_group.selectAll('.heat-map-rect').data(grid_data)
    .enter().append('rect')
    .attr('height', rect_attr.height)
    .attr('width', rect_attr.width)
    .attr('class', 'heat-map-rect bordered')
    .attr('x', function(d){ return x_scale(d.key); })
    .attr('y', function(d){ return d.row*cell_size; })
    .attr('rx', rect_attr.rx)
    .attr('ry', rect_attr.ry)
    .style("fill", function(d,i) {
        if(d.value == 0)
            return '#eee';
        else
            return color_scale(d.value);
    })

动画

加一点儿颜色过渡动画,没有例二那么花哨。d3做这种简单的过渡动画很方便。

要用默认颜色填充,修改上面的fill属性。

var heat_map_rect = grid_group.selectAll('.heat-map-rect').data(grid_data)
    .enter().append('rect')
    .attr('height', rect_attr.height)
    .attr('width', rect_attr.width)
    .attr('class', 'heat-map-rect bordered')
    .attr('x', function(d){ return x_scale(d.key); })
    .attr('y', function(d){ return d.row*cell_size; })
    .attr('rx', rect_attr.rx)
    .attr('ry', rect_attr.ry)
    .attr('fill', '#eee');

使用d3.transition加入动画,包括delay延迟效果(但延迟不明显,有待后续改进)。

heat_map_rect.transition()
    .delay(function(d,i){
        var cur_row = d.row;
        var cur_row_pos = cur_row / row_count;
        var cur_col = i - d.row*time_count;
        var cur_col_pos = cur_col / time_count;
        if(cur_row_pos < cur_col_pos )
            return cur_col_pos/time_count*5000;
        else
            return cur_row_pos/time_count*5000;
    })
    .duration(1000)
    .style("fill", function(d,i) {
        if(d.value == 0)
            return '#eee';
        else
            return color_scale(d.value);
    });

文字标签

纵坐标标签为项目名

var row_label = grid_group.selectAll('row-label').data(row_name)
    .enter().append('text')
    .attr('x', 0)
    .attr('y', function(d,i){ return i*cell_size; })
    .text(function(d){ return d;})
    .style('text-anchor', 'end')
    .attr('transform', 'translate(-6,'+cell_size/1.5+')');

横坐标标签为小时

var time_label = grid_group.selectAll('.time-label').data(time_range)
    .enter().append('text')
    .attr('class', 'time-label')
    .attr('x', function(d,i){ return x_scale(i); })
    .attr('y', 0)
    .text(function(d){
        if(d<12)
            return d+'a';
        else
            return d+'p';
    })
    .style("text-anchor", "middle")
    .attr('transform', 'translate('+cell_size/2+',-6)');

交互

仿照例二,鼠标指向某矩阵网格时,高亮该网格边框,并在下面绘制相应项目的条形图,高亮条形图中对应的当前数值。条形图默认显示第一行的数据,第一行数据被看成是总体数值。

创建条形图

svg中rect坐标为左上角点,y轴正方向竖直向下,而一般条形图y轴正方向为竖直向上,所以条形图中rect的纵坐标为最大高度减去rect的高度。

var bar_data_max = d3.max(bar_data,function(d){return d.value});
var bar_y_scale = d3.scale.linear().domain([0, bar_data_max]).range([0,bar_chart_attr.max_bar_height]);

条形图

var bar = bar_chart_group.selectAll('.heat-map-bar').data(bar_data)
    .enter()
    .append('rect')
    .attr('class', 'heat-map-bar')
    .attr('x', function(d,i){ return i*bar_chart_attr.item_width; })
    .attr('y', function(d,i){ return bar_chart_attr.max_bar_height - bar_y_scale(d.value) })
    .attr('width', bar_chart_attr.bar_width)
    .attr('height', function(d,i){ return bar_y_scale(d.value) })
    .attr('fill', function(d,i){ return '#ddd' });

添加标签与上面相同。

更新条形图

条形图展示某一行的数据,改变行号时需要更新条形图。重新设置条形图的data,并设置变化的属性,包括y坐标、height高度和fill填充色。当然,数据最大值也发生改变,需要重新计算比例尺。

var bar_data_max = d3.max(bar_data,function(d){return d.value});
var bar_y_scale = d3.scale.linear().domain([0, bar_data_max]).range([0,bar_chart_attr.max_bar_height]);
var bar = bar_chart_group.selectAll('.heat-map-bar').data(bar_data)
    .transition().duration(1000)
    .attr('y', function(d,i){ return bar_chart_attr.max_bar_height - bar_y_scale(d.value) })
    .attr('height', function(d,i){ return bar_y_scale(d.value) })
    .attr('fill', function(d,i){ return '#ddd' });

鼠标事件相应

当鼠标指向(mouseover)网格时,如果当前行改变则更新条形图,然后高亮条形图中对应小时的数据。
当鼠标移出(mouseout)网格时,同样检测当前行是否改变(多余?),恢复默认颜色。
用css来表示高亮效果。

rect.bordered{
    stroke: #E6E6E6;
    stroke-width:2px;
}
rect.bordered.hover{
    stroke: black;
    stroke-width:2px;
}

添加对mouseover和mouseout的事件监听。使用classed设置类。

heat_map_rect.on('mouseover', function(d, i){
        d3.select(this).classed("hover", true);
        if(current_bar_chart_row != d.row) {
            update_bar_chart(d.row);
            current_bar_chart_row = d.row;
        }
        var child_index = i - d.row*time_count+1;
        bar_chart_group.selectAll('.heat-map-bar').filter(":nth-child("+child_index+")")
            .classed('hover', true);
    })
    .on('mouseout', function(d, i){
        d3.select(this).classed("hover", false);
        if(current_bar_chart_row!=0){
            current_bar_chart_row = 0;
            update_bar_chart(current_bar_chart_row);
        }
        var child_index = i - d.row*time_count+1;
        bar_chart_group.selectAll('.heat-map-bar').filter(":nth-child("+child_index+")")
            .classed('hover', false);
    });

示例