学习d3.js(以下都简称d3)也有一段时间了,运行d3做了几个项目。我发现中文的d3教程很少,国外资料多但要求有一定的英文阅读能力(推荐网址:http://bl.ocks.org/mbostock),于是就萌发了写一个d3实际运用系列文章的想法,现在开始付之行动。在系列中,我会用d3+html5 canvas实现一些实际效果(如统计结果展示,地图数据展示等),希望可以跟大家共同学习交流。
代码我公布在git.cschina.com上,大家可以clone到本地运行,地址是:http://git.oschina.net/0604hx/d3lesson
运行环境是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 这个类。
最终效果图
最终的效果图如下(当鼠标移动到省份可以看到具体的信息)
(根据录取率排行,颜色深的代表录取率越高)
(根据高考人数排行,颜色深的代表高考人数越多)
如何实现?
1. 画出中国地图
整个地图是用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中都创建了什么?
2. 根据排行对省份进行颜色分配
接着,就要根据排序规则对省份上色了。先看看统计数据是怎么样的(就是第一步中转换过来的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); }
3. 创建提示
鼠标移动到省份上,可以显示具体的信息(这个功能是很实用的!客户绝对是需要的)
首先,先定义好用来显示提示的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>
然后用d3填充数据
/** * 创建提示条 * 提示的创建大致有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("?ll", "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); }) ; }
详细的代码请到:http://git.oschina.net/0604hx/d3lesson