Chapter 3. Scales, Axes and Lines(比例尺、坐标轴与线)
本章介绍的是各种尺度、坐标轴的处理以及折线的使用。诚如开篇所言,绘制可视化图表时要考虑的一个基本问题,就是怎样将实际的数据值,以恰当比例的像素大小与颜色呈现在页面上。D3 在这方面做了大量优化工作,一起来看看吧。
示例1:公交故障间距关于碰撞受伤率的散点图
本例的背景,是想探讨纽约市的公交故障频率、公交相撞事件及乘客交通事故之间是否存在关联关系。毕竟在纽约这样一个交通管网错综复杂的的大都会,偶发的故障或交通事故可能会引发一系列连锁反应,如果能从统计角度找出一些关联因素,则可以提前采取应对措施。这里选取的问题切入点,是想看看有人员伤亡的公交碰撞事件发生率,是否会对公交事故的间距产生影响(真是不明觉厉的脑回路。。。好吧,只是学学 D3 散点图的绘制,其他的问题就饶过作者吧)。
言归正传。为了解决映射比例的问题,D3 提供了一个工具函数 d3.extent()
,返回一个包含最小值与最大值的数组,省去了手动计算取值范围的问题。
然后将实际值的范围作定义域(domain),绘图区的范围作值域(range),得到两个轴向上的比例尺。
接着再将数据绑定到 SVG 绘图区的 circle 元素上,利用定义好的比例尺绘出各个数据点。
最后是利用 D3 的坐标轴函数生成 x 轴和 y 轴,以及对应的坐标轴名称,设置好 CSS 样式,即大功告成。
完整代码如下:
<body>
<h2>Ch3 - Example 1 | Bus Breakdown, Accident, and Injury</h2>
<script src="/demos/js/d3.js"></script>
<script>
let json = null;
function draw(data) {
"use strict";
// visualization code goes here
json = data;
var margin = 50,
width = 700,
height = 300;
// plot circles
var x_extent = d3.extent(data, function(d){ return d.collision_with_injury });
var y_extent = d3.extent(data, function(d){ return d.dist_between_fail });
var x_scale = d3.scale.linear()
.domain(x_extent)
.range([margin, width - margin]);
var y_scale = d3.scale.linear()
.domain(y_extent)
.range([height - margin, margin]);
d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height)
.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('r', 5)
.attr('cx', function(d){ return x_scale(d.collision_with_injury) })
.attr('cy', function(d){ return y_scale(d.dist_between_fail) });
// Add axes
// x
var x_axis = d3.svg.axis().scale(x_scale);
d3.select('svg')
.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + (height - margin) + ')')
.call(x_axis);
// 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);
// Axis title
d3.select('.x.axis')
.append('text')
.text("collisions with injury (per million miles)")
.attr('x', (width / 2) - margin)
.attr('y', margin / 1.5);
d3.select('.y.axis')
.append('text')
.text('mean distance between failure (miles)')
.attr('transform', 'rotate(-90, -43, 0) translate(-280)');
}
d3.json("/demos/data/bus_perf.json", draw);
</script>
<style>
.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;
}
</style>
</body>
效果如下:
本例的一个小坑,出现在 D3.js 的版本替换上,已知的变更包括:
- 线性比例尺的创建,由
d3.scale.linear()
改为了d3.scaleLinear()
; - 坐标轴的创建方式:
- 旧版:
d3.svg.axis().scale(x_scale)
、d3.svg.axis().scale(y_scale).orient('left')
; - 新版:
d3.axisBottom(x_scale)
、d3.axisLeft(y_scale)
(当然还有d3.axisTop
和d3.axisRight
,更贴近声明式风格)
- 旧版:
但第一次改动后的最终效果却不甚理想:
可以看到坐标轴上的数字加粗了,且坐标轴标签没有显示出来。前者可以在 CSS 样式中禁用 .tich{ stroke: black; }
来修复,后者则要按 F12 查看 text 标签是否生成了(确实生成了):
然后在 JS 中或 CSS 中加入填充色:
- 在
JavaScript
中修复:
d3.select('.x.axis')
.append('text')
.attr('fill', 'black')
// ...
d3.select('.x.axis')
.append('text')
.attr('fill', 'black')
// ...
- 在
CSS
中修复:
text {
fill: black;
}
效果如下:
注意到 y 轴名称显示不全,需再次手动调整 y 标签旋转后的纵向平移量,实测发现改为 translate(-100)
比较合适:
附:新版写法完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<title>Ch3 - Example 1 | Getting Started with D3</title>
</head>
<body>
<h2>Ch3 - Example 1 | Bus Breakdown, Accident, and Injury</h2>
<script src="/demos/js/d3.v6.js"></script>
<script>
let json = null;
const draw = data => {
"use strict";
// badass visualization code goes here
json = data;
const margin = 50,
width = 700,
height = 300;
// plot circles
const x_extent = d3.extent(data, d => d.collision_with_injury);
const y_extent = d3.extent(data, d => d.dist_between_fail);
const x_scale = d3.scaleLinear() // v6.7.0
.domain(x_extent)
.range([margin, width - margin]);
const y_scale = d3.scaleLinear() // v6.7.0
.domain(y_extent)
.range([height - margin, margin]);
d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height)
.selectAll('circle')
.data(data)
.join(enter => enter.append('circle')
.attr('r', 5)
.attr('cx', d => x_scale(d.collision_with_injury))
.attr('cy', d => y_scale(d.dist_between_fail))
);
// Add axes
// x
const x_axis = d3.axisBottom(x_scale); // v6.7.0
d3.select('svg')
.append('g')
.attr('class', 'x axis')
.attr('transform', `translate(0, ${(height - margin)})`)
.call(x_axis);
// y
const y_axis = d3.axisLeft(y_scale); // v6.7.0
d3.select('svg')
.append('g')
.attr('class', 'y axis')
.attr('transform', `translate(${margin}, 0)`)
.call(y_axis);
// Axis title
// x
d3.select('.x.axis')
.append('text')
.attr('fill', 'black') // v6.7.0
.text("collisions with injury (per million miles)")
.attr('x', (width / 2) - margin)
.attr('y', margin / 1.5);
// y
d3.select('.y.axis')
.append('text')
.attr('fill', 'black') // v6.7.0
.text('mean distance between failure (miles)')
.attr('transform', 'rotate(-90, -43, 0) translate(-100)');
}
d3.json("/demos/data/bus_perf.json").then(draw)
.catch(console.error);
</script>
<style>
.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;
}
</style>
</body>
</html>
示例2:绘制两个地点的转门日均流量对比图
(未完待续)