Getting Started with D3
CHAPTER 3 第三章 刻度 轴与线
注:源码压缩包请到https://github.com/mikedewar/getting_started_with_d3下载,里面有学习所用的json文件
在我们制作web页面时,其中我们需要解决的一个基本问题就是,如何将我们掌握的数据转换为一个像素值或颜色值。对于统计报表可视化而言,这确实是一个复杂的过程:我们需要处理带有数字和顺序的单位,对数单位,时间单位......不尽其数。
好消息是:D3的开发人员把所有这类问题变得很简单!
车辆抛锚,事故和损伤
纽约,有着错综复杂的交通线路供大量人群使用,MTA(大都会捷运局总线)统筹整个交通系统,当然,如此大规模人群,有些交通事故是不可避免的。MTA会定期发布它的车辆抛锚和事故数据,现在,就让我们来看看车辆故障,相撞和人员事故方面是否有联系。
为了做到这一点,我们决定用基本散点图来显示,这是一种将圆点放到网页特定位置的一种图。在上一章,我们用了HTML元素(div标签)来做了一个条形图,这回我们来用SVG元素来做。
SVG不能用IE9以下的浏览器支持,这是需要大家注意的
首先下载数据文件,地址是 http://www.mta.info/developers/data/Performance_XML_Data.zip,找到data/bus_perf.json,里面的三项: “Collisions with Injury Rate,” “Mean Dis-tance Between Failures,” 和 “Customer Accident Injury Rate.” ,格式基本如下所示:
[{"collision_with_injury": 2.1, "dist_between_fail": 924.0, "customer_accident_rate": 4.12
},{"collision_with_injury": 4.2, "dist_between_fail": 1924.0, "customer_accident_rate": 2.92
},{"collision_with_injury": 1.2, "dist_between_fail": 2924.0, "customer_accident_rate": 3.32
},{"collision_with_injury": 1.3, "dist_between_fail": 3924.0, "customer_accident_rate": 1.52
},{"collision_with_injury": 3.8, "dist_between_fail": 4924.0, "customer_accident_rate": 3.12
},{"collision_with_injury": 2.2, "dist_between_fail": 2024.0, "customer_accident_rate": 1.12
}]
译者注:上述所列测试文件早已更新,不再用json格式而更改为xml且内容更多,这是一些我自己做的测试数据,直接用即可。
SVG小教程
- SVG是一种基于XML规范,用于绘制的东西。篇幅所限我们对于SVG不能多讲,但是以下是你必须知道的:所有的SVG元素都应写在一个有 width 和 height 属性的 svg 标签中,你的可视化内容就在这里,出了这个标签的SVG元素都由DOM管理,但是不可视。
- SVG认为从封闭元素左上角算起是其(0.0)坐标点,如果我们规划图形时计划左下角为(0.0),这将是一件很头疼的事情。
- 不同于HTML元素,我们规定所有的SVG元素的属性,如:形状属性,位置属性,都会放到tag标签中,就像用css那样。对于形状元素,在浏览器读他们之前就必须指出一组相关属性。
- 有一点很重要:SVG元素能像web页面其他元素一样,接受css样式调用!当然,css不能控制形状有几个边角或其他几何属性,能控制的是颜色,边框,字体等等。这就给了我们极大的便利,我们可以把注意力集中在布局和可视化技术的精确性上,把设计内容留在后面(或甩给删除这个的朋友或同事)。
- 在SVG中,g 表示 group ,我们用 g 元素来分组,移动一组对象。
用extent方法和scale方法将数据映射到需要的单位
首先,我们设定700*300的可视化面积,margin为50像素,以便有足够的空间写刻度和说明。
var margin = 50,width = 700,height = 300;
这样设定SVG的可视化空间可能会在后面设定刻度是有点儿小麻烦,在以后的章节中,我们将用更健壮的方案来做。我们用跟第二章一样的格式来做,唯一不同的是代码是写在SVG元素标签中的。我们把宽高设好,再为数据点增加一个圆点:
d3.select("body").append("svg").attr("width", width).attr("height", height).selectAll("circle").data(data).enter().append("circle");
为了让浏览器渲染圆点,我们需要指定圆点的半径和坐标(相对于封闭元素左上为(0.0),不要忘了),这涉及到扩大我们的数据所以单位用像素是说的通的。在D3语言中这意味着我们需要创建一个function来从数据范围(输入的)转换成一个像素范围(输出的)。这正是scale对象要做的事情。
首先,我们用d3.extent找到已知数据的最大最小值:
var x_extent = d3.extent(data, function(d){return d.collision_with_injury});
d3.extent是一个D3为我们提供的便利方法,用于返回它内部参数的最大最小值,在这个例子中,返回了损伤率的一个集合。可以看到,作为extent第二个参数,我们设置了一个function来选择哪一个属性是我们想要的。然后我们构建刻度:
var x_scale = d3.scale.linear().range([margin,width-margin]).domain(x_extent);
现在x_cale在[40,660]这个范围内映射已知数据的范围,这个意思是说我们能用x_scale作为一个function来接受已知数据的从最大值到最小值并且输出值在40到660之间。
y轴一样的操作,只不过把范围数据更改为失败间距离。坐标长度(range)是可视化空间的高度到底部的margin:
var y_extent = d3.extent(data, function(d){return d.dist_between_fail});
var y_scale = d3.scale.linear().range([height-margin, margin]).domain(y_extent);
注意,范围(domain)是指已知数据的最大最小值之间的范围,而坐标长度(range)指在可视化空间的y轴最大值(300)到margin值(50)的范围。 这表示我们已知数据点的最大值映射到50,而最小值映射到300,有点奇怪,是么?不过这恰好是我们所说的可视化空间的零点坐标是在封闭元素的左 上的佐证,虽然我们希望初始化零点在左下!通过反向映射可以达到我们想要的效果。这两条线有了以后,我们就可以轻松的在空间为圆点布局了,要知道它们已经在我们指定的margin内合理的定位了。如何用这些刻度呢?我们把它们作为方法调用,其功用为读取已有数据并输出正确像素单位值:
d3.selectAll("circle").attr("cx", function(d){return x_scale(d.collision_with_injury)}).attr("cy", function(d){return y_scale(d.dist_between_fail)});
为了浏览器渲染,我们必须指出此圆的半径(如果不给出必要属性是无法渲染的):
d3.selectAll("circle").attr("r", 5);
这样就得到了一张不是十分详实的圆圈图。
增加轴线
为了这幅散点图更富有表现力,我们需要一些轴线。D3库提供了减轻我们负担的轴线构造器方法。这里用到的是我们上面创建的sacle对象
var x_axis = d3.svg.axis().scale(x_scale);
这里创造了一个function,一调用便会返回一组由刻度,轴,说明组成的SVG元素。因为刻度已经传递给了轴,所以它自己知道需要多大的范围以及怎样沿着它的长度放置文字。需要我们做的仅仅是考虑把它放进去:
d3.select("svg") .append("g") .attr("class", "x axis") .attr("transform", "translate(0," + (height-margin) + ")") .call(x_axis);
写完后发生了两件事儿,第一件,我们用一个SVG transform把一个轴组移到了图像底部,SVG transform的作用是移动或旋转一个存在的元素,SVG translate transform仅仅用于移动元素;当我们想移动一组元素时,这是非常好用的一个方法。这里一组作为x轴的元素向右移动0元素,从顶部移动height-margin像素到底部。与我们的图像底部坐标一致;刻度和说明文字要在margin里显示。
注意:这个包含x轴的一组元素有两个类:x 和 axis。选哪个用都可以
第二件事儿是我们用了.call()方法来画轴。它所做的是运行了一个名叫time-axis的方法,通过把当前的选择项(这组元素)作为参数。总之,这两个命令定位并画出了我们的x轴,请看:
我们用同样的方法添加y轴:
var y_axis = d3.svg.axis().scale(y_scale).orient("left");
d3.select("svg") .append("g") .attr("class", "y axis") .attr("transform", "translate(" + margin + ", 0 )") .call(y_axis);
与x轴不同,这里我们用了orient方法,把轴的方向设为“left”,然后我们移动y轴,距离为到闭合元素左侧“margin”像素。
我们有两个需要美化的地方。一个是y轴的说明刻度被SVG的可视化窗口截掉了一部分(译者注:原作者提供的数据和图样,y轴数字确被遮住了一部分,我做的并没有遮住),第二是Chrome浏览器的缺省渲染出来的圆圈圈实在太丑了!我们用CSS来解决这些问题:
.axis path{fill:none;stroke: black;
}
.axis {font-size:8pt;font-family:sans-serif;
}
.tick {fill:none; stroke:black;
}
circle{stroke:black;stroke-width:0.5px;fill:RoyalBlue;opacity:0.6;
}
CSS使我们的图表好看多了。D3库真正做到了设计与技术分离,让你专注于开发应用。
增加轴标题
为了客户们能更好的理解我们费劲做出的图表,我们需要添加轴标题。这不是直接归D3管,我们可以简单的放置一些SVG文本元素。x轴标题的添加十分简单:
d3.select(".x.axis").append("text").text("collisions with injury (per million miles)").attr("x", (width / 2) - margin).attr("y", margin / 1.5);
这里我们选择了x轴组,插入了一个文本元素,指定了文本的内容,以及它的x坐标和y坐标的相对位置。通过尝试不同的比率来检测哪一个是最好的!
增加y轴有点点小难度,因为我们要先旋转在平移,我们要先旋转-90度,再让文字朝下走。具体看图便知:
d3.select(".y.axis").append("text").text("mean distance between failure (miles)").attr("transform", "rotate (-90, -43, 0) translate(-280)");
我们可以用Chrome的开发工具或火狐的firebug调试,这样能迅速定位元素。
至此,我们创建了一个失败和高发损伤率的关系图,但是这个关系图内容并不明确,下面让我们关注一些新应用!
画出验票闸机交通图
在纽约,进出地铁都需要过验票闸机,一名乘客购买车票并到闸机口验票,解锁相当于运行一次,每次运行都会被MTA收集并定期公布出数据。
让我们查看2012年2月10日,星期五的一份儿资料,地址在http://www.mta.info/developers/data/nyct/turnstile/turnstile_120211.txt 。每一行都是一天中某一位置缩产生的闸机的一组数据。分析这些文件简直就是噩梦:请用代码解析器来查看吧。接下来,经过我们的努力,我们得到了一个名为turnstile_traffic的json文件,是穿过时代广场和中央地铁站的人流平均数,这是纽约最大的两个站。文件顶级包含两个键grand_central 和 times_square,每个键指向一个列表对象,就像这样:.
{"count": 87.36111111111111, "time": 1328371200000
}
初始化可视化空间
我们先来制作一个闸机运行总次数和时间之间的散点图,再用线把点连接起来,使其成为一个不错的时间序列图。那么第一个需要解决的问题就是把屏幕上的像素与时间戳和闸机平均运行次数映射到一起。时间戳是从1970年1月1日起的毫秒级的字段,闸机平均运行次数从10一直到超过到1000。
还是先来做我们的可视化空间:
var margin = 40,width = 700 - margin,height = 300 - margin;
d3.select("body") .append("svg").attr("width", width+margin).attr("width", height+margin).append(g).attr("class","chart");
然后为Times Square and one for Grand Central分别做enter方法的选择器(selection)