D3 学习笔记三:创建柱状图,第二部分

(原文地址:https://bost.ocks.org/mike/bar/2,原文作者:Mike Bostock

简介:出于个人学习目的,计划将 D3 的学习资料逐步翻译为系列文章,鉴于本人水平有限,如有不到或错误之处,欢迎大家指正,谢谢!

在本教程的前一部分我们用 HTML 只做了一个基础的柱状图,在本篇中,我们将使用 SVG 来实现该柱状图,同时通过加载外部的 TSV 格式数据来让应用情景更加真实。

SVG 介绍

HTML 绝大部分局限于方框形状的绘制,而 SVG 则对贝塞尔曲线、渐变、剪切和蒙版提供了强大支持。虽然我们不需要 SVG 的所有扩展特性来实现一个简单的柱状图,但是当我们设计可视化时,学习 SVG 语法是对我们增加视觉编程词汇的必备扩充。

像其他事物一样,丰富的功能往往伴随代价,庞大的 SVG 规范局限了我们的视野,但要记住,我们并不需要掌握所有的特性才能入门。通过浏览这些例子是学习新技术的快乐方法。

虽然 SVG 和 HTML 有明显的差异,但二者也分享许多共通性。您可以将 SVG 标记语言直接嵌入网页代码,也可以通过浏览器的开发者工具检查 SVG 元素。SVG 可以使用 CSS 进行样式化,但二者使用了不同的标签名称(比如 SVG 使用 fill 而不是 background-color)。当然,不像 HTML,SVG 元素必须相对容器的左上角进行布局,SVG 不支持流动性布局或文字自动转行。

手动编码图表

在我们使用 JavaScript 构建新图表之前,让我们重新复习静态 SVG 规范:

<!DOCTYPE html>
<style>

.chart rect {
  fill: steelblue;
}

.chart text {
  fill: white;
  font: 10px sans-serif;
  text-anchor: end;
}

</style>
<svg class="chart" width="420" height="120">
  <g transform="translate(0,0)">
    <rect width="40" height="19"></rect>
    <text x="37" y="9.5" dy=".35em">4</text>
  </g>
  <g transform="translate(0,20)">
    <rect width="80" height="19"></rect>
    <text x="77" y="9.5" dy=".35em">8</text>
  </g>
  <g transform="translate(0,40)">
    <rect width="150" height="19"></rect>
    <text x="147" y="9.5" dy=".35em">15</text>
  </g>
  <g transform="translate(0,60)">
    <rect width="160" height="19"></rect>
    <text x="157" y="9.5" dy=".35em">16</text>
  </g>
  <g transform="translate(0,80)">
    <rect width="230" height="19"></rect>
    <text x="227" y="9.5" dy=".35em">23</text>
  </g>
  <g transform="translate(0,100)">
    <rect width="420" height="19"></rect>
    <text x="417" y="9.5" dy=".35em">42</text>
  </g>
</svg>

像之前一样,样式表可以修改 SVG 元素的颜色及其他属性,但是与 div 可以使用流动布局布置标签的位置不同,svg 元素必须使用相对于原点的固定偏移量绝对定位。

我们经常被 SVG 的样式和属性二者差异所迷惑,样式属性的完整列表在 SVG 规范之中被明确定义。我们可以简单的做如下归类:几何形状(如方框的长与宽)必须使用属性定义,而外观内容(如填充颜色)可以使用样式表定义。虽然您可以用属性定义一切,为了确保内联样式能够与样式表的正确交互,我建议您使用样式表表达外观部分。

SVG 要求文字内容被明确放置在 text 元素内,但由于其 text 元素不支持填充和边界,文字的定位必须与柱子顶端保持 3 像素的偏移量,使用 dy 偏移量来让文字垂直居中。

虽然与 HTML 的许多规范都不同,我们看到的图表结果与前一篇的图表保持了一致。

自动编码图表

下面,让我们用 D3 来构建图表,下面的代码将会很眼熟:

<!DOCTYPE html>
<meta charset="utf-8">
<style>

.chart rect {
  fill: steelblue;
}

.chart text {
  fill: white;
  font: 10px sans-serif;
  text-anchor: end;
}

</style>
<svg class="chart"></svg>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>

var data = [4, 8, 15, 16, 23, 42];

var width = 420,
    barHeight = 20;

var x = d3.scale.linear()
    .domain([0, d3.max(data)])
    .range([0, width]);

var chart = d3.select(".chart")
    .attr("width", width)
    .attr("height", barHeight * data.length);

var bar = chart.selectAll("g")
    .data(data)
  .enter().append("g")
    .attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });

bar.append("rect")
    .attr("width", x)
    .attr("height", barHeight - 1);

bar.append("text")
    .attr("x", function(d) { return x(d) - 3; })
    .attr("y", barHeight / 2)
    .attr("dy", ".35em")
    .text(function(d) { return d; });

</script>

为了能根据数据数量计算图表高度,我们用 Javascript 设置了 svg 元素的尺寸,这样图表的高度就可以可以自动适应,而不是让图表高度决定柱子宽度。

每个柱子由一个包含recttextg 元素组成,我们使用数据接合(enter 选择器)来为每个数据创建图表,然后将 g 元素垂直移动,从而为布置柱子本身及其标签创造一个自身原点。

因为每个 g 元素只有一个 rect 和一个 text 元素,我们可以将其直接追加至 g 而不需要更多的数据接合,仅仅在基于数据创建不同数量的子元素时需要数据接合。被追加的 recttext 继承了它们父元素的数据,所以我们可以直接使用该数据计算柱子宽度和标签位置。

加载数据

下面让我们通过获取一个独立的数据文件并嵌入,来让这个例子更加真实。外部的数据文件将该实现与数据进行分离,让该实现对不同数据集甚至实时变化的数据的重用更加容易。

分栏符分隔的数据集(TSV)是一个常用的表格数据结构,该数据可以通过微软 Excel 表格或其他表格程序、乃至通过文本编辑器手写获得。数据的每一行代表一个表格行,每个行则包含通过分栏符分隔的不同列。第一行表头表达了对应列的名字,与之前全是数据的表格不同,我们将增加一个名字列,内容如下所示:

name        value
Locke       4
Reyes       8
Ford        15
Jarrah      16
Shephard    23
Kwon        42

想要在浏览器中使用该数据,我们需要从网络服务器下载该文件并处理它,将该文件的内容转换为可用的 JavaScript 对象。幸运的是,我们可以通过一个简单的函数实现该任务:d3.tsv

加载数据增加了新的复杂度:下载内容是异步的。当我们呼叫 d3.tsv 时,该函数会立即返回而不会理会在后台继续下载的内容。只有在未来某时下载完成时,回调函数会提供下载的数据,或汇报下载的失败。通过以下代码我们表达执行情况:

// 1. 这里在下载开始前运行

d3.tsv("data.tsv", function (error, data) {
  // 3. 回调函数,这里的代码会在下载成功后最后运行
});

// 2. 这里的代码接着运行,此时下载还未完成

所以,我们需要将图表的实现分为两步:第一,我们尽可能在数据获得前初始化一切,为了数据下载后页面绘制就绪,最好在页面加载时设置好图表尺寸;第二,我们在回调函数中继续完成未完成的绘制。

我们按如下重新整理代码:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.chart rect {
  fill: steelblue;
}
.chart text {
  fill: white;
  font: 10px sans-serif;
  text-anchor: end;
}
</style>

<svg class="chart"></svg>

<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>

var width = 420,
    barHeight = 20;

var x = d3.scale.linear()
    .range([0, width]);

var chart = d3.select(".chart")
    .attr("width", width);

d3.tsv("data.tsv", type, function(error, data) {

  x.domain([0, d3.max(data, function(d) { return d.value; })]);

  chart.attr("height", barHeight * data.length);

  var bar = chart.selectAll("g")
      .data(data)
    .enter().append("g")
      .attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });

  bar.append("rect")
      .attr("width", function(d) { return x(d.value); })
      .attr("height", barHeight - 1);

  bar.append("text")
      .attr("x", function(d) { return x(d.value) - 3; })
      .attr("y", barHeight / 2)
      .attr("dy", ".35em")
      .text(function(d) { return d.value; });
});

function type(d) {
  d.value = +d.value; // coerce to number
  return d;
}

</script>

所以,这里变化的地方是:虽然我们在同样的位置声明了 x-scale,我们无法在数据加载前定义其数值范围,因为该范围取决于数据的最大值。因此,我们再回调函数内声明了该范围,同样的,虽然图表宽度可以静态设置,图表的高度仍取决于在回调函数中获取的数据的长度。

现在,我们的数据集包含了名称与数值二者,我们必须用 d.value 而不是 d 获取数值,每个数值点都是一个对象而不是数字。其对应的 Javascript 表达如下所示:

var data = [
  {name: "Locke",    value:  4},
  {name: "Reyes",    value:  8},
  {name: "Ford",     value: 15},
  {name: "Jarrah",   value: 16},
  {name: "Shephard", value: 23},
  {name: "Kwon",     value: 42}
];

在之前图表中我们所有用 d 取值的地方都必须用 d.value 取代,特别是由于我们需要通过缩放决定柱子的宽度,我们必须使用函数来获取数值传入缩放组件: function (d) { return x(d.value); }。同时,在计算数据集最大值时,我们必须为 d3.max 提供一个函数来告诉它如何评估每个点的数据。

由于使用了外部数据,我们还有一个要处理的地方:数据类型。名字成员包含的是字符串而数据成员包含的是数字,不幸的是,d3.tsv 并不足以自动检测和转换数据类型,我们必须指定一个作为 d3.tsv 的第二个参数的函数来进行转换,该转化函数可以根据每一行修改数据,从而令其变成更适合的表达方式:

function type (d) {
  d.value = +d.value;  // 强制转换为数字
  return d;
}

类型转换并不是严格需要,但它是一个很好的注意。默认情况下,TSV 和 CSV 的所有列都是字符串,如果您忘记将字符串转换为数字,JavaScript 可能不会做到你所期待的结果,比如将 "1" + "2" 返回 "12" 而不是 3。相似的,如果你将字符串而不是数字排序,d3.max 的字符串排序结果可能会吓到你。

下一篇:第三部分

虽然我们的柱状图没有我们之前创建的手写代码那么令人印象深刻,但这里我们介绍了两个非常重要的现实中数据呈现的内容:SVG 和外部数据。在这里,我们更好的完成了之前的图表,但在下一篇教程之中,我们将介绍坐标轴及图表风格。

个人笔记

在本篇教程之中,作者通过用全新的方法来实现与之前相同的柱状图例子让读者登堂入室,进入了 D3 图形编程真正的大门,同时对之前的许多概念进行了复习和强化。

本篇文章除了增加 SVG 和数据获取内容之外,与前一篇之间没有带入很高的学习难度,但这里呈现的图表绘制过程,将在读者未来的所有图表应用之中其到模板式的作用。

从模块组成上来讲,D3 的诸多模块让我们初学者容易摸不清头脑,也不知道该从哪里入手学习绘制图表,但本篇教程是一个入门的很好的例子,虽然内容并不复杂,却描述了所有 D3 图表绘制工程的一般过程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342