💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
>[info] 原文链接:https://bost.ocks.org/mike/bar/ 假如你有一些数据,比如是一组数字: ~~~ var data = [4, 8, 15, 16, 23, 42]; ~~~ 条形图是一种简单而且能[准确感知](http://flowingdata.com/2010/03/20/graphical-perception-learn-the-fundamentals-first/)的方式,来可视化这种数据。这个教程涵盖了如何使用[D3 JavaScript库](http://d3js.org/)制作一个条形图。首先,我们创建一个基本的HTML版本,然后是一个基本完成的SVG[[Scalable Vector Graphics](http://www.w3.org/Graphics/SVG/)]图表,最后在视图中加入动画转换。 这个教程假设你懂得一些web开发:如何编译一个web页面,并能够在浏览器中预览它,如何加载[D3库](http://d3js.org/)等等。 你也可以便捷的fork这个[CodePen template](http://codepen.io/mbostock/pen/Jaemg)来开始上路。 #### **选择一个元素** 在纯粹的JavaScript【vanilla JavaScript】中,你基本上都是一次处理一个元素。比如,为了创建一个`div`元素,设置它的内容,然后将它添加到`body`中: ~~~ var div = document.createElement("div"); div.innerHTML = "Hello, world"; document.body.appendChild(div); ~~~ 使用D3(或者jQuery以及其它的库),你可以调用`selections`来处理一组相关的元素。一并处理元素给予了`selections`强大的功能;你可以处理单独一个元素,或者同时操作多个元素,并不需要大幅重构你的代码。虽然这看上去是一个很小的改变,但消除循环和其它的控制流可以使你的代码更加简洁。 一个`selection`可以通过多种方式创建。通常你会调用一个[selector](http://www.w3.org/TR/selectors-api/)创建一个,这是一个特殊字符串通过属性来识别想要的元素,比如通过名称或类名("div"或者".foo"),你也可以为一个单独的元素创建一个selection: ~~~ var body = d3.select("body"); var div = body.append("div"); div.html("Hello, world!"); ~~~ 你也可以很容易的在多个元素上执行同样的操作: ~~~ var section = d3.selectAll("section"); var div = section.append("div"); div.html("Hello, world!"); ~~~ #### **链方法【Chaining Methods】** selections的另一个方便之处就是方法链【method chaining】:选择器方法,比如[`selection.attr`](https://github.com/mbostock/d3/wiki/Selections#wiki-attr),返回当前选择器。这可以让你很容易在相同元素上执行多种操作。比如为了设置body的文本颜色,和背景色,不使用方法链的情况下,你也许是这样写的: ~~~ var body = d3.select("body"); body.style("color", "black"); body.style("background-color", "white"); ~~~ "Body, body, body!"这种方式和下面的方法链相比,去除了复读机式的重复: ~~~ d3.select("body") .style("color", "black") .style("background-color", "white"); ~~~ 你会发现我们甚至没有用到`var`来声明选中的body元素。执行任意操作后,这个选择器就可以被丢弃掉。方法链允许你书写很短的代码(同时不需要再花费时间,苦思冥想变量的名字)。 然而方法链还有一个小技巧:大多数操作返回的还是相同的选择器,但有些方法返回的则是一个新的选择器!比如,[`selection.append`](https://github.com/mbostock/d3/wiki/Selections#wiki-append)会返回一个新的选择器,包含了新添加的元素。这方便你对新增的元素执行链操作。 ~~~ d3.selectAll("section") .attr("class", "special") .append("div") .html("Hello, world!"); ~~~ >[info] 方法链的推荐缩进模式是:保留当前选择器的方法为4个空格缩进,而改变当前选择器的方法为2个空格缩进 由于方法链只能在文档层级中向下传递,所以可以使用`var`来保存选择器的引用,以便后续回溯用到。 ~~~ var section = d3.selectAll("section"); section.append("div") .html("First!"); section.append("div") .html("Second"); ~~~ #### **手动编写一个图表** 现在考虑下如何不使用JavaScript创建一个条形图。毕竟,这里只有6个数字,所以直接手写一些div元素并不困难,将它们的宽度设置为数据的倍数,就做出了一个图表。 ~~~ <!DOCTYPE html> <style> .chart div { font: 10px sans-serif; background-color: steelblue; text-align: right; padding: 3px; margin: 1px; color: white; } </style> <div class="chart"> <div style="width: 40px;">4</div> <div style="width: 80px;">8</div> <div style="width: 150px;">15</div> <div style="width: 160px;">16</div> <div style="width: 230px;">23</div> <div style="width: 420px;">42</div> </div> ~~~ 看上去效果如下: ![](https://box.kancloud.cn/0e7eff76b9c7e058945bcf9f467fed49_446x158.png) 这个图表有一个`div`作为容器,然后每一个条带是一个子`div`。这些子div有一个蓝色的背景色,和一个白色的前景色,于是创建出了数值右对齐的条带标签。你可以通过移除掉容器`div`,更加简化这个实现。但是通常情况下,你的页面除了图表还要包含其他内容,所以有一个图表容器,你就可以在不影响页面其他部分的情况下,设置图表的位置和样式。 #### **自动生成一个图表** 显然硬编码对于大多数数据集都是不可行的,而且这个教程的目的是教会你,如何自动从数据中生成图表。所以现在,让我们使用D3创建同样的结构,从一个空页面开始,这个页面仅包含一个`div`,其类名为“chart”。下面的代码选中了图表容器,然后对于每一个条带,添加了一个有指定宽度的子div: ~~~ d3.select(".chart") .selectAll("div") .data(data) .enter().append("div") .style("width", function(d) { return d * 10 + "px"; }) .text(function(d) { return d; }); ~~~ 尽管有一部分熟悉了,但这个代码介绍了一个新的重要概念 -- 数据加载【data join】。让我们来拆析一下,把上面简洁的代码拆解重写一遍,看看它是如何工作的。 首先,我们使用一个类选择器,选中了图表容器。 ~~~ var chart = d3.select(".chart"); ~~~ 然后我们通过定义把数据加入到哪个选择器里,来初始化数据加载。 ~~~ var bar = chart.selectAll("div"); ~~~ 数据加载是一个常用的模式,可以用来数据改变时,创建,更新,或者销毁元素。可能看上去会比较奇怪,但是这个方式的好处就是你只需要学习一种单一的模式,就可以管理页面。所以无论你要构造一个静态的图表,还是动态的,具有流体转换的,甚至[对象持久化](https://bost.ocks.org/mike/constancy/)的图表,你的代码基本上都是一样的。**可以把初始化选择器考虑为声明你想要创建的元素(参见“[Thinking with Joins](https://bost.ocks.org/mike/join/)”)** 下一步我们加载数据到之前定义的选择器中,使用[selection.data](https://github.com/mbostock/d3/wiki/Selections#wiki-data)方法。 ~~~ var barUpdate = bar.data(data); ~~~ 因为我们知道选择器是空的,返回的update和exit选择器也是空的,我们只需要处理enter选择器,它代表了没有存在元素的新数据。我们通过添加到enter选择器中来初始化这些不存在的元素。 ~~~ var barEnter = barUpdate.enter().append("div"); ~~~ 现在我们设置每一个新条带的宽度,设置为相关联的数据值,d的倍数。 ~~~ barEnter.style("width", function(d) { return d * 10 + "px"; }); ~~~ 因为这些元素是通过数据加载创建的,每一个条带已经绑定了数据。我们基于它的数据设置了每个条带的尺寸,通过传递一个方法来计算宽度样式属性。 最后,我们使用一个方法来设置每一个条带的文本内容,生成一个标签。 ~~~ barEnter.text(function(d) { return d; }); ~~~ D3的选择器操作比如[attr](https://github.com/mbostock/d3/wiki/Selections#wiki-attr),[style](https://github.com/mbostock/d3/wiki/Selections#wiki-style)和[property](https://github.com/mbostock/d3/wiki/Selections#wiki-property),允许你指定数值,可以指定为一个常量(所有选中的元素值相同),也可以指定为一个方法(对于每一个元素分别计算)。如果一个特殊属性的值应当依赖于元素相关的数据,那就使用一个方法来计算它;否则,如果对于所有的元素值都一样,那就可以使用一个字符串,或者数字就足够了。 #### **缩放到适合的大小** 这段代码的一个缺点是[魔数](http://en.wikipedia.org/wiki/Magic_number_(programming)#Unnamed_numerical_constants)10,这个数用来缩放数据值到合适的像素宽度。这个数字依赖于数据域(数据最小值和最大值,这里分别是0, 42),以及图表的期望宽度(420),但是显然这些依赖都隐含于数字10中。 我们可以使这些依赖更加明确,并且使用一个线性缩放【[linear scale](https://github.com/mbostock/d3/wiki/Quantitative-Scales)】来省略魔数。D3的缩放指定了从数据空间(域【domain】)到显示空间(范围【range】)的一个映射: ~~~ var x = d3.scale.linear() //v4中API变更为d3.scaleLinear() .domain([0, d3.max(data)]) .range([0, 420]); ~~~ 虽然这里的`x`看上去像一个对象,但它其实也是一个方法返回缩放后的显示值,这个显示值处于域中一个给定数据值的范围内。比如,输入一个4则返回40,输入一个16则返回160。为了使用这种新的缩放方式,只需要简单将硬编码乘法替换为调用这个缩放函数: ~~~ d3.select(".chart") .selectAll("div") .data(data) .enter().append("div") .style("width", function(d) { return x(d) + "px"; }) .text(function(d) { return d; }); ~~~ #### **下一步:第2节** 这里展现的基本条形图是很容易实现的,当然也有明显的局限性。一个条形图应该有网格线用来比较数值,而且可能你会更喜欢垂直的条形图。或者你会想要一个不同的图表类型,比如饼状图或流体图。为了更好地视觉体验,你将需要SVG。SVG将在下一节介绍。