vivian blog

Trip Of D3.js

前言

从freecodecamp中得知了d3.js,在官网上看到了那么多高大上的例子,不禁想要多加了解它

d3.js介绍

  • D3.js是一套javaScript函数库,包含一整组操纵画图很好的辅助工具,还有很方便element操作模型

  • 需掌握javascript,html和css与svg


废话不多说,先上例子(用d3的3.5.3版本)


柱形图

定义变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var data = chart.data;
d3.select('#note').text(chart.description);
barWidth = Math.ceil(width / data.length);
minDate = new Date(chart.from_date);
maxDate = new Date(chart.to_date);
var margin = {
top: 5,
right: 10,
bottom: 30,
left: 75
},
width = 1000 - margin.left - margin.right, //所有小矩形所占用的宽度
height = 500 - margin.top - margin.bottom,//小矩形最大的高度
barWidth = Math.ceil(width / data.length);

设置svg画布

  • 为了改变轴相对于基址图的位置,可以指定g元素上的 transform 变换属性,此处改变的是svg图
1
var chart = d3.select('#chart').attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom).append('g').attr('transform', "translate(" + margin.left + "," + margin.top + ")");

坐标轴渲染

  • 先定义x,y轴比例尺,本例使用d3.time.scale()[日期比例尺]与d3.scale.linear()[线性比例尺],然后再设置范围和定义域,注意:svg的x,y轴的方向是由左上角向右与下延伸
1
2
3
4
5
var x = d3.time.scale().domain([minDate, maxDate]).range([0, width]);//d3.time必须传入Date对象
var y = d3.scale.linear().range([height, 0]).domain([0, d3.max(data, function(d) {
return d[1];//注意range([height,0])
})]);
  • x,y轴的渲染设置,d3.svg.axis.scale()是将比例尺应用在坐标轴上,orient()指刻度的方向,ticks()刻度出现的频率,而ticks(d3.time.year,5)指的是每5年一个刻度,同样的代表时间的还有d3.time.minute/d3.time.seconds
1
2
3
var xAxis = d3.svg.axis().scale(x).orient('bottom').ticks(d3.time.year, 5),
yAxis = d3.svg.axis().scale(y).orient('left').ticks(10, "")//ticks(10)表示将y值刻度分成10份
  • 渲染x,y轴,attr(y,)表示相对于svg坐标y轴的位置,而attr(dy,)是偏离于y的值其中attr('tranform','translate(left,top)')是偏离(x,y)当前位置,然后call()相应的坐标轴渲染设置
1
2
3
4
5
//x轴
chart.append("g").attr("class", "x axis").attr("transform", "translate(0," + height + ")").call(xAxis);
//y轴
chart.append("g").attr("class", "y axis").call(yAxis).append('text').attr('transform', "rotate(-90)").attr('y', 6).attr('dy', '0.8em').style('text-anchor', 'end').text("Gross Domestic Product, USA");//style('text-anchor','end')表示在尾部对齐,添加单位可直接append('text')并做出相对应的位置偏移即可

画图

  • 设置选择器,此处通过append(‘rect’)来画矩形图
1
chart.selectAll(".bar").data(data).enter().append("rect").attr("class", "bar");
  • 设置矩形具体信息
1
2
3
4
5
6
7
d3.selectAll('.bar').attr('x', function(d) { //x坐标
return x(new Date(d[0]));
}).attr('y', function(d) { //y坐标
return y(new Date(d[1]));
}).attr('height', function(d) {
return height - y(d[1]); //svg的y坐标是在左边且往下
}).attr('width', barWidth);
  • 为其绑定事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var div = d3.select('.container').append('div').attr('class', 'title').style('opacity', 0);//小矩形信息
d3.selectAll('.bar').on("mouseover", function(d) {
var rect = d3.select(this);
rect.attr("class", "mousehover");
var currentDate = new Date(d[0]),
year = currentDate.getFullYear(),
month = currentDate.getMonth(),
dollars = d[1];
div.transition().duration(200).style("opacity", 0.9);//动画,duration表示变化时间
div.html("<span class = 'tip'>" + formatCurrency(dollars) + "&nbsp;Billion </span><br><span class = 'year'>" + year + ' - ' + Months[month] + '</span>')
.style('left', (d3.event.pageX + 5) + 'px')//d3.event.pageX是svg(position X)
.style('top', (d3.event.pageY - 50) + 'px');
}).on('mouseout', function() {
var rect = d3.select(this);
rect.attr('class', 'mouseoff');
div.transition().duration(500).style('opacity', 0);
});

散布图

此处便只post上与柱形图不同的核心代码

绘制比例尺

  • 由于x轴单位为Minutes Behind Fastest Time,而date对象中并没有直接地对分钟的提取,故需要使用tickFormat()函数来设置刻度格式,事实上,在此时,ticks指定的参数也要传递给scale.tickFormat方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var convertTime = function(s) {//按min:sec显示
var sec = s%60,min = (s-sec)/60;
sec = (sec<10)? '0' + sec : sec;
return min + ':' + sec;
}
var xAxis = d3.svg.axis().scale(x).orient('bottom').tickFormat(convertTime), //x轴
yAxis = d3.svg.axis().scale(y).orient('left');
//x轴绘制
svg.append('g')
.attr('class', 'x axis')
.attr('transform', "translate(0," + height + ")")
.call(xAxis)
.append('text')
.attr('class', 'label')
.attr('x', width)
.attr('y', -6)
.style('text-anchor', 'end')
.text('Minutes Behind Fastest Time');
//y轴绘制
svg.append('g')
.attr('class', 'y axis')
.call(yAxis)
.append('text')//添加y轴单位
.attr('class', 'label')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.8em')
.style('text-anchor', 'end')
.text('Ranking');

画图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
svg.selectAll('.dot')//此处若无该选择器,则返回一个空选择器
.data(data)
.enter().append('circle')
.attr('class', 'dot')
.attr('cx', function(d) {
return x(d.Seconds - minTime);
})
.attr('cy', function(d) {
return y(d.Place);
})
.attr('r', 3.5)
.style('fill', function(d) {
if (d.Doping === '') {
return '#035';
}
return '#EB3349';
}).on('mouseover',function(d) {
var circle = d3.select(this);
circle.attr('class','mouseover');
div.html('<span class = "des" >'+'name: '+d.Name+' Nationality: '+d.Nationality+'</span><br><span class = "doping">'+'Doping:'+d.Doping+'</span>'+'<br>'+'<span class = "URL">'+'URL'+d.URL+'</span>')
.style('left',width /2 +'px').style('top', height*0.8+'px').style('opacity',1);
}).on('mouseout',function(){
var circle = d3.select(this);
circle.attr('class','mouseoff');
div.style('opacity',0);
});

热图

分析:本例使用的d3.json()来获取数据,凡是画图表,重中之重仍是比例尺的绘制。现在让我们来分析一下如何绘制该比例尺吧。
首先是x轴的绘制,此处与我们上述的代码都相似

1
2
var x = d3.time.scale().domain([minYear, maxYear]).range([0, width]),//必须使用date对象传入
xAxis = d3.svg.axis().scale(x).orient('bottom').ticks(d3.time.years, 10);

然后便是monthLabel的设置,它的方位是在y轴位置,但此处无需按照我们先前设置比例尺后,再绘制坐标轴,因为无需显示刻度线。

1
2
3
4
5
6
7
8
9
10
11
var monLabels = svg.selectAll('.monthLabel').data(Months).enter().append('text')
.attr('class','monthLabel')
.text(function(d) {
return d;
})
.attr('x', 0)
.attr('y', function(d, i) {
return i * gridHeight+gridHeight/2;
})
.style('text-anchor', 'end')
.attr('transform', 'translate(-6,'+gridWidth/1.5+')');

自定义颜色比例尺,该比例尺乃是热图的特点,通过自定义颜色来确定温度的范围,让数据瞬间一目了然。
此处需要用到d3.scale.quantile()量化比例尺,定义域是用于可视化的数据维度,值域则是输出的可视化维度,用在此处便恰当好处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var colorScale = d3.scale.quantile()
.domain([minVariance + baseTem, maxVariance + baseTem]).range(colors);
var dataPicker = svg.selectAll('.dataset-button')
.data(colorScale.quantiles());
//rect
dataPicker.enter().append('g').attr('class', 'dataPicker')
.append('rect')
.attr('x', function(d, i) {
return legendElementWidth * i + (width - legendElementWidth * buckets)+8;
})
.attr('y', height + 50)
.attr('width', legendElementWidth)
.attr('height', gridHeight / 2)
.style('fill', function(d, i) {
return colors[i];
});
//text
dataPicker.append('text')
.attr('class', 'scales')
.text(function(d) {
return Math.floor(d*10)/10;
})
.attr('x', function(d,i) {
return legendElementWidth * i + (width - legendElementWidth * buckets);
})
.attr('y', height + 50 + gridHeight)
.style('text-anchor','center');
});

然后便开始画图了,注意d3.event.pageX表示当前交互物件相对于svg的坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var temps = svg.selectAll('.years').data(dataTem)
.enter().append('rect').attr('class','years')
.attr('x', function(d) {//此处也可使用x(new Date(d.year,0))来获得x属性值
return ((d.year - lowYear)*gridWidth);
})
.attr('y', function(d) {
return ((d.month - 1) * gridHeight);
})
.attr('rx', 0)
.attr('ry', 0)
.attr('width', gridWidth)
.attr('height', gridHeight)
.style("fill", function(d) {
return colorScale(d.variance + baseTem);
}).on('mouseover', function(d) {
div.transition().duration(100).style('opacity', 0.8);
div.html('<span class = "year">' + d.year + '</span>' + ' - ' + '<span class = "month">' + Months[d.month - 1] + '</span><br><span class = "temp">' + formatTem(d.variance + baseTem) + '&#8451' + '</span><br><span class = "variance">' + d.variance + '&#8451' + '</span>')
.style('left', d3.event.pageX - $('.tooltip').width() / 2 + 'px')
.style('top', d3.event.pageY - 80 + 'px');
}).on('mouseout', function(d) {
div.transition().duration(200).style('opacity', 0);
});

力导学图

这时候我需要介绍一种关于d3的力导学图布局给大家,d3.layout.force(),力导学图可以反映事物之间的关系,下面以国家间的关系为例。

Show National Contiguity with a Force Directed Graph

作品
现在让我来说一下思路

  • 布局(数据转换)
1
2
3
4
5
6
var force = d3.layout.force()
.nodes(nodes) //指定节点数组
.links(edges) //指定连线数组
.size([width,height]) //指定作用域范围
.linkDistance(150) //指定连线长度
.charge([-400]); //相互之间的作用力
  • 使力学作用作用生效
1
force.start();//开始作用
  • 绘制
    • line 线段
    • circle 节点
1
2
3
4
5
6
7
8
9
10
11
12
13
//绘制line
var svg_edges = svg.selectAll('.line').data(edges).enter().append('line').attr('class','line');
//绘制节点
var svg_nodes = d3.select('#chart').select('#flagbox').selectAll('.node').data(nodes).enter().append('img')
.attr('class',function(d) {
return 'flag flag-'+d.code;
}).on('mouseover',function(d) {
toolTip.style('opacity',1);
toolTip.html(d.country).style('left',d3.event.pageX+'px').style('top',(d3.event.pageY-28)+'px');
}).on('mouseout',function(d) {
toolTip.style('opacity',0);
}).call(force.drag);//使得节点能够拖动
  • 更新节点和连线位置(因为力导向图是时刻变化的,重中之重)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
force.on('tick',function() {
//更新连线坐标
svg_edges.attr('x1',function(d) {
return d.source.x;
}).attr('y1',function(d) {
return d.source.y;
}).attr('x2',function(d) {
return d.target.x;
}).attr('y2',function(d) {
return d.target.y;
});
//更新节点坐标,需稍微改变各节点坐标,动态显示
svg_nodes.style('left',function(d){
if(d.x-8>0)
return (d.x-8)+'px';
}).style('top',function(d){
return (d.y-5)+'px';
});
});

效果示例

  • 柱形图

See the Pen Visualize Data with a Bar Chart by vivian (@leoCecilia) on CodePen.

  • 散布图

See the Pen Visualize Data with a Scatterplot Graph by vivian (@leoCecilia) on CodePen.

  • 热点图

See the Pen Visualize Data with a Heat Map by vivian (@leoCecilia) on CodePen.

  • 力导向图

See the Pen Force Directed Graph by vivian (@leoCecilia) on CodePen.