企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
>[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, "%"); ~~~