>[info] 原文链接:https://bost.ocks.org/mike/bar/3/
在前几节我们制作了一个基本的条形图,分别使用[HTML](https://bost.ocks.org/mike/bar/),然后改用了[SVG](https://bost.ocks.org/mike/bar/2/)实现;现在,我们将要提高显示效果,将图表旋转为列式展览,并且加上坐标轴。我们依然使用一个真实的数据集,这个数据集展示了在英语中26个字母的相对使用频率。
![](https://box.kancloud.cn/a8feb101ff629143c7e98dcda66469fa_932x490.png)
#### **旋转为列【Rotating into Columns】**
将一个条形图旋转为列大都涉及到将x和y进行交换。然而,一系列小的附带变化也是需要的。这是直接操作SVG而不是使用一个高阶可视化语法(比如[ggplot2](http://ggplot2.org/))的代价。另一方面,SVG提供了更好的可定制化;并且SVG是一个web标准,所以我们可以使用浏览器的开发者工具比如元素查看器,并且可以使用SVG做可视化之外的事情。
当将x比例尺改名为y比例尺时,range域(即值域)变为[height, 0]而不再是[0, width]。这是因为SVG的坐标系原点位于浏览器的左上角。我们想让0值位于图表的底部,而不是顶部。类似的,我们需要使用“`y`”和“`height`”属性来定位条带`rect`元素,然而之前我们仅需要设置“`width`”。(“`x`”属性的默认值是0,之前的条带都是左对齐)
我们之前将`barHeight`变量乘以每个数据点的索引来生成固定高度的条带。这样的结果是图表的高度依赖于数据集的大小。但是这里我们需要相反的表现:图表的宽度是固定的,而每个条带的宽度可变。所以不再固定`barHeight`,而是通过可用的图表宽度除以数据集大小【`data.length`】来计算`barWidth`。
最后,条带标签必须按列放置而不是按行,在列顶部下方居中。新的“`dy`”属性值为“.75em”,将标签锚定在文本的上限高度【[cap height](http://en.wikipedia.org/wiki/Cap_height)】左右,而不是在基线位置。
~~~
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.chart rect {
fill: steelblue;
}
.chart text {
fill: white;
font: 10px sans-serif;
text-anchor: middle;
}
</style>
<svg class="chart"></svg>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
var width = 960,
height = 500;
var y = d3.scale.linear()
.range([height, 0]);
var chart = d3.select(".chart")
.attr("width", width)
.attr("height", height);
d3.tsv("data.tsv", type, function(error, data) {
y.domain([0, d3.max(data, function(d) { return d.value; })]);
var barWidth = width / data.length;
var bar = chart.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(" + i * barWidth + ",0)"; });
bar.append("rect")
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.attr("width", barWidth - 1);
bar.append("text")
.attr("x", barWidth / 2)
.attr("y", function(d) { return y(d.value) + 3; })
.attr("dy", ".75em")
.text(function(d) { return d.value; });
});
function type(d) {
d.value = +d.value; // coerce to number
return d;
}
</script>
~~~
#### **对序数编码【Encoding Ordinal Data】**
不像数量型数据可以比较数值上的大小,做减法或除法,序数值通过等级进行比较。字母就是一种序数;在字母表中,A出现在B前面,B在C前面。D3的线性,幂,和对数比例尺用来编码数量型数据,而[序数比例尺](https://github.com/mbostock/d3/wiki/Ordinal-Scales)则是用来编码序数数据。因此我们可以使用一个序数比例尺,简单的通过字母来定位条带。
在其最明确的形式中,一个序数比例尺是从一个离散的数据集(比如名字)到一个对应的离散显示集(比如像素位置)的映射。类似于数值型的比例尺,这2个集合也分别称为定义域【domain】和值域【range】
~~~
var x = d3.scale.ordinal()
.domain(["A", "B", "C", "D", "E", "F"])
.range([0, 1, 2, 3, 4, 5]);
~~~
x("A")的结果就是0,x("B")的结果是1,等等。在指定定义域和值域时,最重要的就是值的顺序:定义域中的i号元素,会映射到值域中的i号元素。
手动枚举每个条带的位置是十分枯燥的,所以替代的我们可以使用[rangeBands](https://github.com/mbostock/d3/wiki/Ordinal-Scales#wiki-ordinal_rangeBands)或者[rangePoints](https://github.com/mbostock/d3/wiki/Ordinal-Scales#wiki-ordinal_rangePoints)将一个连续范围转化为一组离散值。`rangeBands`方法计算范围值,以便将图表区域划分为均匀间隔,均匀大小的区带,就像条形图那样。类似的rangePoints方法计算均匀间隔的范围值,比如散点图。例如:
~~~
var x = d3.scale.ordinal()
.domain(["A", "B", "C", "D", "E", "F"])
.rangeBands([0, width]);
~~~
如果`width`是960,x("A")现在就是0,而x("B")则是160,等等。这些位置是每一个条带的左边界,而`x.rangeBand()`返回的是每个条带的宽度。但是rangeBands也可以使用可选的第3个参数来为条带之间添加补间【padding】,`rangeRoundBands`则是输出的值会舍入为最接近的整数,整数值有助于将视觉元素与像素网格对其,保证图形的边缘清晰锐利。比较如下代码:
~~~
var x = d3.scale.ordinal()
.domain(["A", "B", "C", "D", "E", "F"])
.rangeRoundBands([0, width], .1); // .1为inner padding,即每档宽度的10%
~~~
>[warning] 计算过程:960 / 6 = 160,每档宽度为160,档内间距为16,所以x("A")从第17个像素开始,即为17,除去7个间距,那么每个条带的实际宽度为(960 - 7 \* 16) / 6 = 141
现在x("A")是17,每个条带的宽度是141。并且,在我们的定义域中不需要硬编码字母进去,我们可以从数据中使用[array.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)和[array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)来计算它们。这些都放一起,就形成了如下代码:
~~~
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.chart rect {
fill: steelblue;
}
.chart text {
fill: white;
font: 10px sans-serif;
text-anchor: middle;
}
</style>
<svg class="chart"></svg>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
var width = 960,
height = 500;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var y = d3.scale.linear()
.range([height, 0]);
var chart = d3.select(".chart")
.attr("width", width)
.attr("height", height);
d3.tsv("data.tsv", type, function(error, data) {
x.domain(data.map(function(d) { return d.name; }));
y.domain([0, d3.max(data, function(d) { return d.value; })]);
var bar = chart.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d) { return "translate(" + x(d.name) + ",0)"; });
bar.append("rect")
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.attr("width", x.rangeBand());
bar.append("text")
.attr("x", x.rangeBand() / 2)
.attr("y", function(d) { return y(d.value) + 3; })
.attr("dy", ".75em")
.text(function(d) { return d.value; });
});
function type(d) {
d.value = +d.value; // coerce to number
return d;
}
</script>
~~~
#### **准备边距【Preparing Margins】**
序数比例尺通常和D3的坐标轴组件一块使用,用来快速显示刻度标记,从而提高图表的易读性。但在添加坐标轴之前,我们需要清除边距中的一些空间。
[按照惯例](https://bl.ocks.org/mbostock/3019563),D3中的边距被指定为一个拥有顶部,右边,底部,左边属性的对象。然后,图表区域的整体尺寸,包括外边距,通过减去外边距来计算可用于图形标记的内部尺寸。比如,对于一个960x500的图表来说,合理值为:
~~~
var margin = {top: 20, right: 30, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
~~~
因此,960和500分别就是整体宽度和高度,而计算出的内部`width`和`height`则分别为890和450。这些内部规格可以用来初始化比例尺的值域【ranges】。为了将外边距应用到SVG容器中,我们对外设置了SVG元素的宽度和高度,然后添加一个`g`元素,使其偏移图表原点一个左上角外补间的距离。
~~~
var chart = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
~~~
随后添加到`chart`中的元素,都会继承这个外边距。
#### **添加坐标轴**
我们定义一个坐标轴,将其绑定到已经存在的x比例尺上,并声明一个朝向。这样我们的x坐标轴就会出现在条带的底部,这里我们使用的是`"bottom"`朝向。
~~~
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
~~~
生成的`xAxis`对象可用来渲染多个坐标轴,只需要多次调用`selection.call`就行。可以想象它是一个橡皮图章,可以在任何需要的地方打印出坐标轴。坐标轴元素相对于一个本地原点来定位,所以为了放在需要的位置,我们需要在包裹它的`g`元素上设置`"transform"`属性:
~~~
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
~~~
坐标轴容器还应该有个类名,方便我们应用样式。这里的“axis”名字是随便起的;你可以使用任何名字。复合类名,比如“x axis”,可用来依据维度来使用不同的坐标轴样式,同时在所有维度上还可以保留一些共享的样式。
坐标轴组件包含了一个`path`元素,用来展示定义域【domain】,还包含了多个类名为“.tick”的`g`元素,用来指示每个刻度标记。一个刻度依次包含了一个`text`标签,和一个`line`标记。大多数的D3例子都使用了如下的极简风格:
~~~
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
~~~
这就创建了一个[R工程](http://www.r-project.org/)的怀旧坐标轴:
![](https://box.kancloud.cn/2dc15f21e97e0a27aeee1ff8d2e295e6_441x50.png)
但是,坐标轴是高度可定制的。下面这个更加精巧的坐标轴是仿[ggplot2](http://ggplot2.org/)的风格:
![](https://box.kancloud.cn/47adf92e90c201d0d916cc1ed7eb2949_442x76.png)
除了样式之外,你还可以通过选择其元素,并在坐标轴创建后修改它们,来进一步定制一个坐标轴的外观;一个坐标轴的元素是它的公共API一部分。上面的[ggplot2风格的坐标轴](https://bl.ocks.org/mbostock/4349486)使用了2个平铺的坐标来渲染,一个在图表区域内部,而另一个在外面,位于底部外补间中。主刻度和副刻度使用了不同的样式。
有了坐标轴,我们现在可以移除条带中的标签。完整代码如下:
~~~
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.bar {
fill: steelblue;
}
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
</style>
<svg class="chart"></svg>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
var margin = {top: 20, right: 30, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var chart = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.tsv("data.tsv", type, function(error, data) {
x.domain(data.map(function(d) { return d.name; }));
y.domain([0, d3.max(data, function(d) { return d.value; })]);
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
chart.append("g")
.attr("class", "y axis")
.call(yAxis);
chart.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.name); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.attr("width", x.rangeBand());
});
function type(d) {
d.value = +d.value; // coerce to number
return d;
}
</script>
~~~
#### **传词达意【Communicating】**
在这点上,我恐怕要让你失望了。
我努力解释了图表构造中的技术细节,但我却掩盖了有效可视化中的一个重要部分:有效的沟通。一个图表再好看,如果它不能表达任何意思,那么就毫无用处!我们必须标记图表,给读者足够的内容来理解它。
这个问题比你想象的更加普遍。当可视化数据时,很容易隐藏掉甚至忘掉你对数据集的额外理解以及初衷。你知道这是一个关于英语字母相对频率的条形图。但是除非你明确注明,你的读者不一定能知晓这些。标签,标题,图例和其它说明要素对于理解是至关重要的。一个标题可以添加到Y轴上,通过添加一个text元素,并定位到需要的位置上。
~~~
chart.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Frequency");
~~~
单位适当的数值格式,可以裁剪待显示的数据,这同样有助于提高图表的易读性。因为我们展示的是相对频率,百分数就比默认的小数更适合一些。一个[格式化字符串](https://github.com/mbostock/d3/wiki/Formatting)作为[axis.ticks](https://github.com/mbostock/d3/wiki/SVG-Axes#wiki-ticks)的第2个参数将会定制刻度格式,并且比例尺将会针对刻度间隔自动选择合适的精度。
~~~
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10, "%");
~~~