选择集负责创建和更新图形元素;比例尺负责将数据值映射为像素、颜色等视觉属性。
比例尺的作用
拿到的数据和要画的像素、颜色不是一回事。QPS 从 0 到 5000,圆点半径想控制在 0 到 40 像素之间,直接把 QPS 数值当半径用肯定不行,5000 像素的圆早就超出屏幕了。中间需要一个函数,把 QPS 的取值范围换算成像素的取值范围。
D3 里这个函数由 d3.scale.linear() 生成:
var radiusScale = d3.scale.linear()
.domain([0, 5000])
.range([0, 40]);
radiusScale(2500); // 20
radiusScale(5000); // 40
radiusScale(0); // 0domain 是数据本身的取值范围,range 是映射之后的目标范围。d3.scale.linear() 调用完返回的是一个函数,radiusScale 存的就是这个函数。之后每次调用 radiusScale(某个 QPS 值),返回的都是按线性比例换算出来的像素数。domain 和 range 两头的区间长度不需要相等,中间会自动做线性插值,2500 落在 0-5000 的正中间,换算到 0-40 的 range 里也是正中间,也就是 20。
映射圆点半径和颜色
假设有一批服务节点的 QPS 数据:
var services = [
{ name: "waimai-order", qps: 1200 },
{ name: "waimai-cart", qps: 300 },
{ name: "waimai-search", qps: 4800 },
{ name: "waimai-user", qps: 50 }
];先算出这批数据里 QPS 的实际范围,不手写死区间,用 d3.max 和 d3.min 取:
var maxQps = d3.max(services, function(d) { return d.qps; });
var minQps = d3.min(services, function(d) { return d.qps; });
var radiusScale = d3.scale.linear()
.domain([minQps, maxQps])
.range([8, 40]);range 从 8 开始而不是 0,是因为半径为 0 的圆点在图上看不见,最小的节点也得有个能点得中的大小。
选择集绑定数据之后,圆的半径直接调用这个比例尺:
svg.selectAll("circle")
.data(services)
.enter()
.append("circle")
.attr("cx", function(d, i) { return i * 100 + 50; })
.attr("cy", 100)
.attr("r", function(d) { return radiusScale(d.qps); });颜色的映射方式一样,range 里换成两个颜色字符串:
var colorScale = d3.scale.linear()
.domain([minQps, maxQps])
.range(["#fee8c8", "#e34a33"]);
svg.selectAll("circle")
.attr("fill", function(d) { return colorScale(d.qps); });当 d3.scale.linear() 的 range 使用两个颜色字符串时,D3 将颜色作为可插值的范围处理。中间值对应的颜色在两个端点颜色之间插值,无需手动计算渐变。
ordinal scale 与等宽分类
上面这种是连续数值的映射,服务名称这种分类数据不适用,没有大小关系,也没法插值。这种场景用 d3.scale.ordinal():
var names = services.map(function(d) { return d.name; });
var xScale = d3.scale.ordinal()
.domain(names)
.rangeRoundBands([0, 400], 0.1);
xScale("waimai-order"); // 某个具体的像素值
xScale("waimai-cart"); // 下一个位置rangeRoundBands 第一个参数是总的像素范围,第二个参数控制条与条之间的间隔比例(严格说是间隔占每段步长,即条宽加间隔之和的比例,0.1 时条与条之间留出大约一成条宽的空隙,值越大间隔越宽)。domain 里有几个名称,这个范围就自动按名称个数等分成几段,每个名称对应一段固定宽度的位置。画柱状图的时候用它给每根柱子分配横坐标,画分类坐标轴的时候也是它。
SVG 坐标系
SVG 里 y 轴正方向是向下的,原点在左上角。日常画图直觉里 y 往上是正方向,这两个是反的。
若直接将比例尺计算出的高度赋给 height,并将 y 设为 0,柱子会从画布顶部向下延伸:
// 错误的写法,柱子从上往下长,值越大越往下
// rect 是已绑定 services 数据的矩形选择集:
// svg.selectAll("rect").data(services).enter().append("rect")
var heightScale = d3.scale.linear()
.domain([0, maxQps])
.range([0, 200]);
rect.attr("y", 0)
.attr("height", function(d) { return heightScale(d.qps); });要让柱子的底边固定在画布底部,y 坐标应为画布高度减去计算出的柱高:
var heightScale = d3.scale.linear()
.domain([0, maxQps])
.range([0, 200]);
rect.attr("y", function(d) { return 200 - heightScale(d.qps); })
.attr("height", function(d) { return heightScale(d.qps); });y 是矩形左上角的坐标,height 是往下延伸的长度,两者配合才能让矩形底边固定在 200 这条线上,看起来才是往上长的。
用 d3.svg.axis() 生成坐标轴
坐标轴本身也是画一堆刻度线和文字,自己一条条画很麻烦,D3 封装了这一步:
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0,200)")
.call(xAxis);d3.svg.axis() 生成一个坐标轴生成器,把比例尺传给它,刻度该画在哪些位置就由它按这个比例尺算出来。orient("bottom") 是指刻度文字画在轴线下方,对应横轴的常见样式。最后一步 .call(xAxis) 是把这个坐标轴生成器应用到一个 <g> 元素上,实际的刻度线和文字就是这时候生成并插入到这个 <g> 里的。
服务大盘 demo 中的比例尺
后续实现的服务大盘 demo使用了四个比例尺:
- 节点半径:
domain是节点 QPS 范围,range是 12 到 30 像素 - 节点颜色:
domain同上,range是蓝到红两个颜色字符串 - 连线粗细和颜色:
domain是调用 QPS 范围,range分别是像素宽度和颜色区间 - 连线上流动白点的速度:
domain是调用 QPS,range是每秒走过的线段比例
有一点超出这篇讲过的范围,补记一下:半径没有用线性比例尺,用了开方比例尺。原因是圆的面积和半径是平方关系,QPS 差 4 倍的两个服务,如果半径也差 4 倍,面积就差了 16 倍,视觉上小的那个小得不成比例。用开方比例尺让面积随 QPS 线性增长,节点之间的大小对比才和 QPS 对比一致。这是可视化里的一个惯例:用圆的大小表达数值时,锚定面积而不是半径。
后续问题
比例尺处理单个图元的视觉属性,不能计算图中节点的位置。服务调用图需要让存在调用关系的节点保持较近距离,并避免节点重叠,因此需要使用 d3.layout.force()。下一步是确定力导向布局的参数,以及如何按线段进度计算移动点的坐标。
