学习d3.js(以下都简称d3)也有一段时间了,运行d3做了几个项目。我发现中文的d3教程很少,国外资料多但要求有一定的英文阅读能力(推荐网址:http://bl.ocks.org/mbostock),于是就萌发了写一个d3实际运用系列文章的想法,现在开始付之行动。在系列中,我会用d3+html5 canvas实现一些实际效果(如统计结果展示,地图数据展示等),希望可以跟大家共同学习交流。
代码我公布在git.cschina.com上,大家可以clone到本地运行,地址是:
运行环境是java 7+,tomcat 7.0.47+(以后会用到websocket,所以需要javaee7 跟 tomcat 7+的支持),IDE 是IntelliJ IDEA 13, 项目的视图使用了freemarker。
这一章讲的是在中国地图上展示2013年大陆各省份高考一本录取率的排行。
首先需要有录取率的相关数据,我从网上复制出来了一份统计数据:
2013年一本录取率排名
1 天津 6.3 1.5447 24.52%
2 北京 7.27 1.7686 24.33%
3 上海 5.3 1.2 22.64%
4 青海 3.6733 0.6837 18.61%
5 山东 50.9 9.351 18.37%
6 宁夏 5.87 1.001 17.05%
7 吉林 15.5 2.2435 14.47%
8 福建 25.5 3.6186 14.19%
9 贵州 24.78 3.4369 13.87%
10 浙江 31.3 4.1887 13.38%
11 陕西 36.65 4.8422 13.21%
12 新疆 15.87 2.05 12.92%
13 云南 23.6 3.0179 12.79%
14 海南 5.6 0.6396 11.42%
15 内蒙古 19.3 2.163 11.21%
16 甘肃 28.3 2.9598 10.46%
17 安徽 51.1 5.1692 10.12%
18 江苏 45.1 4.5085 10.00%
19 湖南 37.3 3.5789 9.59%
20 黑龙江 20.8 1.9931 9.58%
21 重庆 23.5 2.195 9.34%
22 江西 27.43 2.4891 9.07%
23 河北 44.98 4.0602 9.03%
24 湖北 43.8 3.5923 8.20%
25 广西 29.8 2.3 7.72%
26 河南 71.63 4.8655 6.79%
27 广东 72.7 4.3092 5.93%
28 山西 35.8 2.1091 5.89%
29 辽宁 25.4 1.4583 5.74%
30 四川 54 2.849 5.28%
31 西藏 1.89 0.0904 4.78%
这个文件可以在d3lesson中找到
对于有规则的txt数据(如一行一个对象),我写了一个转换工具,可以转成json(json格式在网页中使用较为方便),具体可见:org.nerve.d3lesson.common.tools.impl.TxtToJSONImporter 这个类。
整个地图是用svg的path绘制,那么需要有相应的数据。中国地图的json数据在 /web/data/china.json 中,我们可以用d3的json()方法加载这个json,然后绘制出地图。
加载方法:
d3.json("{json路径}", function(data){
//这里是回调函数,如果加载成功,data就是json对象
//执行drawChina方法绘制地图
});
绘制函数(这里使用过的是墨卡托投影, projection 的调整我暂时没弄透彻,反正是对着屏幕调到满意的位置就好了,如果有朋友知道欢迎解答,万分感谢!)
在绘制过程中,给每个省份对应的path加一个唯一的id,方便以后调用(如修改颜色就是通过id获取path来完成)
// Project from latlng to pixel coords
//使用墨卡托投影
var projection = d3.geo.mercator()
.scale(width/2) //对地图进行缩放
.translate([width / 2, height / 2]) //将地图平移到屏幕中间
.rotate([-110, 0])
.center([0, 37.5]) //设置中心点,调整到屏幕中心
;
// Draw geojson to svg path using the projection
var path = d3.geo.path().projection(projection);
//画出中国地图
function drawChina(ds){
if(!chinaG)
chinaG = container.append("g");
chinaG.selectAll("path")
.data(ds.features)
.enter()
.insert("path")
.attr("id", function(d){
return d.id;
})
.attr("fill", "#000000")
.attr("d", path)
.attr('stroke',setting.strokeColor)
.attr('stroke-width','0.7px')
;
}
看看dom中都创建了什么?
接着,就要根据排序规则对省份上色了。先看看统计数据是怎么样的(就是第一步中转换过来的json数据):
{
"_title": "2013年一本录取率排名",
"datas": [
{
"enter": 1.5447,
"id": "TIANJIN",
"index": 1,
"province": "天津",
"rate": "24.52%",
"total": 6.3
},
{
"enter": 1.7686,
"id": "BEIJING",
"index": 2,
"province": "北京",
"rate": "24.33%",
"total": 7.27
},
{
"enter": 1.2,
"id": "SHANGHAI",
"index": 3,
"province": "上海",
"rate": "22.64%",
"total": 5.3
},
//.....
//剩下的就不列出来了
同样的,我们用d3.json() 方法加载这些数据,然后排序其中的datas数组。
这里要说一下过度颜色,我是这样定义的:
//创建过度颜色,注意上一步的排序是从大到小,那么颜色应该是从深到浅
var rateColors = d3.scale.linear()
.domain([1, 340])
.range([d3.rgb(20, 120, 140),d3.rgb(180, 230, 255)]);
那么可以这样得到一个颜色值: rateColors(index); 传进去的index应该是 1 到 340 之间(当然你传更大或更小的也可以),那么就得到d3.rgb(20, 120, 140),d3.rgb(180, 230, 255) 之间相对应的一个颜色。 如index=1 就得到 d3.rgb(20, 120, 140), index = 340 就得到d3.rgb(180, 230, 255), index=170 就得到两个端点颜色的中间颜色。
最后就是对数据排序,然后更新对应省份的颜色了:
/**
* 根据录取率排序
*/
function sortByRate(){
//首先我们需要对数据进行录取率从大到小的排序
//因为rate 是 xx.xx% 的格式,所以在对比前需要进行parseFloat 的操作
var data = gkData.datas.sort(function(d1,d2){
return parseFloat(d2.rate) - parseFloat(d1.rate);
});
//创建过度颜色,注意上一步的排序是从大到小,那么颜色应该是从深到浅
var rateColors = d3.scale.linear()
.domain([1, 340])
.range([d3.rgb(130, 140, 20),d3.rgb(255, 255, 180)]);
/*
遍历上一步得到是数组
forEach 参数中的 d 就是遍历到的某个数据, i 就是该对象的下标序号,从0开始
*/
data.forEach(function(d,i){
d.sort = i+1;
//通过d.id 来获取中国地图上对应的省份,因为地图中的省份块是根据省份拼音命名的
d3.select("#"+ d.id)
.transition()
.duration(duration)
.delay(10*i)
.attr("fill", rateColors((i+1)*10))
;
});
buildTip(data);
showOnTable(data);
}
/**
* 根据参加高考人数排序
*/
function sortByTotal(){
//首先我们需要对数据进行录取率从大到小的排序
//因为rate 是 xx.xx% 的格式,所以在对比前需要进行parseFloat 的操作
var data = gkData.datas.sort(function(d1,d2){
return d2.total - d1.total;
});
//创建过度颜色,注意上一步的排序是从大到小,那么颜色应该是从深到浅
var rateColors = d3.scale.linear()
.domain([1, 340])
.range([d3.rgb(20, 120, 140),d3.rgb(180, 230, 255)]);
// .range([d3.rgb(30, 40, 160),d3.rgb(180, 160, 255)]);
/*
遍历上一步得到是数组
forEach 参数中的 d 就是遍历到的某个数据, i 就是该对象的下标序号,从0开始
*/
data.forEach(function(d,i){
d.sort = i+1;
//通过d.id 来获取中国地图上对应的省份,因为地图中的省份块是根据省份拼音命名的
d3.select("#"+ d.id)
.transition()
.duration(duration)
.delay(10*i)
.attr("fill", rateColors((i+1)*10))
;
});
buildTip(data);
showOnTable(data);
}
鼠标移动到省份上,可以显示具体的信息(这个功能是很实用的!客户绝对是需要的)
首先,先定义好用来显示提示的div元素
<!--div提示框-->
<div id="tooltip" class="hidden box">
<p>
<strong class="dataHolder" name="province"></strong>
排名:<span class="dataHolder" name="sort"></span>
</p>
<div>
高考人数:<span class="dataHolder" name="total"></span>万
录取率:<span class="dataHolder" name="rate"></span>
</div>
</div>
/**
* 创建提示条
* 提示的创建大致有3种方式
* 1: 给svg元素里面增加一个title元素,
* var t = d3.select(id).append("title").text("我是提示条");
* 这种方法效果不大理想,而且提示单调
*
* 2: 给需要提示的元素添加mouseover, mouseout 事件,当鼠标在该元素上移动时,就显示提示条(动态创建的svg元素),如:
* var t = d3.select(id);
* t.on('mouseover',function(){
* //创建提示条
svg.append("text")
.attr("id", "tooltip")
.attr("x", d3.event.x)
.attr("y", d3.event.y)
.attr("text-anchor", "middle")
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("font-weight", "bold")
.attr("fill", "black")
.text("我是svg的提示条");
})
* });
*
* 3: 类似方法2,但是提示条不是svg元素,而是普通的html元素(如div),动态修改提示框里面的内容跟提示框的x,y坐标
* 达到提示的效果,总体来说这个方法较好,较为灵活,而且可以使用css3,同时不用担心提示框超出svg范围的问题
*
* 所以,在教程中,都是使用这个方法
*/
function buildTip(data){
var t = "#tooltip";
chinaG.selectAll("path")
.data(data, function(d){
return d.id;
})
.on("mouseover",function(d){
d3.select(t)
.style("left", d3.event.x + "px")
.style("top", d3.event.y + "px")
.classed("hidden", false)
.selectAll(".dataHolder")[0]
.forEach(function(h){
h = d3.select(h);
h.html(d[h.attr('name')]);
})
;
d3.select(this)
.attr("opacity", 0.8);
})
.on("mouseout",function(){
d3.select(t).classed("hidden", true);
d3.select(this)
.attr("opacity", 1);
})
;
}
详细的代码请到: