D3.js学习笔记:绘制日历
背景
日历是按日期分布的数据最直观的可视化形式。
时间属性可以和人类日历对应,缤纷为年、月、周、日、小时等多个等级。因此,采用日历表达时间属性,和我们识别时间的习惯相符合。————《数据可视化》 陈为等
书中给出一个使用d3.js绘制的例子
代码请参见d3.js作者的博客《Calendar View》
GitHub也使用日历来显示用户的活跃度。下图是我2014年的活跃度。
参考上述两个示例,我使用d3.js绘制日历图。
实现
用一个例子来说明绘制日历的过程。
需求
与GitHub类似,展示从当前日期之前一年内每天的数据,用颜色表示数值的大小,颜色越浅,数目越小,颜色越深,数目越大。没有数据则用灰色背景表示。
纵轴表示星期,横轴表示周数。与GitHub不同,我设定起始日期始终是一年前的周日。
数据准备
输入数据
当前日期
var end_date = new Date();
测试
end_date
Mon Jan 05 2015 09:24:13 GMT+0800 (中国标准时间)
当前能获得的日期数据
var collect_date_value = [
{date:'2014-10-01',value:0},
{date:'2014-10-02',value:0},
{date:'2014-10-03',value:0},
{date:'2014-10-04',value:1},
{date:'2014-10-05',value:0},
{date:'2014-10-06',value:0},
{date:'2014-10-07',value:5}
];
日期格式:
使用d3.time.format
定义日期格式:
var date_format = d3.time.format("%Y-%m-%d");
测试
date_format(new Date())
"2015-01-05"
数据处理
一年前的周日
d3提供丰富的日期操作函数,使用d3.time.week
返回之前最近的周日。
var start_date = new Date();
start_date.setFullYear(end_date.getFullYear()-1);
start_date = d3.time.week(start_date);
测试
start_date
Sun Jan 05 2014 00:00:00 GMT+0800 (中国标准时间)
日期序列
使用d3.time.days
生成日期序列
var date_range = d3.time.days(start_date, end_date);
测试:
date_range
Array[366]
完整日期数据
将输入的不完整数据扩展到整个日期序列上,默认值为-1,表示没有对应数据。我想到使用map来更新特定索引的值。d3提供d3.map
对象实现map。
设置初值
var date_value_map = d3.map();
date_range.forEach(function(element, index, array){
date_value_map.set(date_format(element), -1);
});
更新数据
var date_value = [];
date_value_map.forEach(function(key,value){
date_value.push({
date:date_format.parse(key), value:value
});
});
组装成最后的输入数据:
var calendarData= {
date_value: date_value,
start_date: start_date,
end_date: end_date,
}
日期网格
横坐标
周数作为横坐标,先用d3.time.weeks
得到周序列,用d3.scale.ordinal
将周序列映射到横坐标长度范围中。
d3.scale.ordinal
用于将离散数据映射到某个区间中。
var week_range = d3.time.weeks(d3.time.week(start_date), end_date);
var len = week_range.length;
var scale = d3.scale.ordinal().domain(week_range).rangeBands([0,len]);
纵坐标
纵坐标为星期序号。
var y_format = d3.time.format("%w");
颜色
使用colorbrewer提供的配色。利用d3.scale.quantile
将整数数值范围映射到5个颜色中。
d3.scale.quantile
用于将数据映射到离散区域中。
var calendar_style = {
color_range: colorbrewer.YlOrRd[5]
};
var max = d3.max(data, function(d){
return d.value;
});
var quantize = d3.scale.quantile().domain([0, max]).range(calendar_style.color_range);
绘制
为每个数据画矩形。
var rect_group=svg.append('g');
var r = rect_group.selectAll('rect').data(data).enter().append('rect')
.attr('x', function(d,i){ return scale(d3.time.week(d.date))*cell_size;})
.attr('y', function(d,i){ return y_format(d.date)*cell_size; })
.attr('width', cell_size)
.attr('height', cell_size)
.style('stroke', '#ccc')
.attr('fill', function(d) {
if(d.value==-1)
return "#eeeeee";
else
return quantize(d.value);
});
交互
提示条
鼠标指向时显示的提示条。d3可以实现tooltip组件,但我为了省事,直接使用bootstrap的tooltip组件。不应该这么混用。
r.attr('data-toggle',"tooltip")
.attr('data-placement','top')
.attr('title',function(d) {
if(d.value==-1)
return date_format(d.date)+ ": No data.";
else if(d.value==0)
return date_format(d.date)+ ": There is no error.";
else if(d.value==1)
return date_format(d.date)+ ": "+ d.value + " error";
else
return date_format(d.date)+ ": "+ d.value + " errors";
}
$('svg').tooltip({
selector:'[data-toggle="tooltip"]',
container:'body'
});
单击事件
单击事件需要与外部组件交互,所以添加到输入参数中。
/* 输入参数 */
calendarData.action.click=function(d){
/* ... */
};
/* 绘图 */
var calendar_action = {};
if(calendarData.hasOwnProperty('action')) {
var param_calendar_action = calendarData.action;
for(var key in param_calendar_action){
calendar_action[key] = param_calendar_action[key];
}
}
/* ... */
r.on('click', function (d, i) {
scope.$apply(function () {
calendar_action.click(d);
});
});
月份分割线
月份分割线的实现完全拷贝自前文calendar的例子。不过因为我的日期范围不是整年,所以无法画出所有月的分割线,最后一个月如果不是最后一天,则无法绘出完整的分割线。所以不画最后一个月(以后有待改进)。
var month_path_group = svg.append('g');
var month_start_day = new Date(start_date);
var month_end_day = new Date(end_date);
month_end_day.setMonth(end_date.getMonth()-1);
month_path_group.selectAll(".month")
.data(function(d) { return d3.time.months(month_start_day, month_end_day); })
.enter().append("path")
.attr("class", "month")
.attr("d", monthPath);
function monthPath(t0){
var t1 = new Date(t0.getFullYear(), t0.getMonth()+1, 0);
var d0 = +y_format(t0);
var w0 = scale(d3.time.week(t0));
var d1 = +y_format(t1);
var w1 = scale(d3.time.week(t1));
return "M" + (w0 + 1) * cell_size + "," + d0 * cell_size
+ "H" + w0 * cell_size + "V" + 7 * cell_size
+ "H" + w1 * cell_size + "V" + (d1 + 1) * cell_size
+ "H" + (w1 + 1) * cell_size + "V" + 0
+ "H" + (w0 + 1) * cell_size + "Z";
}
还需要设置样式表
/* calendar */
.month {
fill: none;
stroke:#000;
stroke-width:2px;
}
月份标注
同样,不标注第一个月和最后一个月。
var month_text_format=d3.time.format('%Y-%m');
var month_text_group = svg.append('g');
month_text_group.selectAll(".month")
.data(function(d) { return d3.time.months(month_start_day, month_end_day); })
.enter().append("text")
.attr("x", function(t0){
var t1 = new Date(t0.getFullYear(), t0.getMonth()+1, 0);
var w0 = scale(d3.time.week(t0));
var w1 = scale(d3.time.week(t1));
return (w0+w1)/2*cell_size;
})
.attr("y", 8*cell_size)
.style("text-anchor", "middle")
.style("font-size", 10)
.text(function(d){
return month_text_format(d);
});
效果
上面例子的演示图片:
使用该组件的又一个示例:
资源
上述代码我已上传到github中,写本文时的文件快照如下:
使用示例
日期控件代码