数据可视化,第 2 部分: 使用 D3 组件进行布局

了解用于绘制各种排列的组件的图形计算

在这个由两部分组成的文章系列中,将学习如何结合使用可缩放矢量图形 (SVG) 和开源 D3 JavaScript 库创建数据可视化。形状、颜色和布局可能对从业务角度理解海量数据具有很大帮助。本文将演示使用 D3 的和您自己的计算结果,通过在画布上排列图形组件来表示数据的各种方式。

Bilal Siddiqui , 自由顾问, XML4Java

Bilal Siddiqui 是一名电子工程师、XML 顾问、技术传播人员,以及经常发表文章的技术作家。他也是 XML4Java.com 的创始人,该公司专注于简化电子商务。自 1995 年从巴基斯坦拉合尔工程技术大学毕业获得电子工程学位后,他就开始设计工业控制系统的软件解决方案。后来,他转向 XML,开始构建基于 Web 和 WAP 的 XML 处理工具、服务器端解析方案以及服务应用程序。从 2006 年开始,他专门研究基于 Java™和 XML 的开源工具和解决方案。作为开源工具的有力支持者,他不仅设计了基于开源工具的解决方案,还在拉合尔大学培训软件和 IT 人员使用开源技术。Bilal 是 JasperReports 3.6 Development Cookbook(Packt Publishing,2010 年)的作者。



2013 年 9 月 23 日

这个由两部分组成的系列文章的 第 1 部分概述了 SVG 和 D3 的结合使用,提供了创建社交媒体的浏览数据可视化表示的一些基本示例。第 2 部分将介绍在 SVG 图形中使用不同图形组件排列或布局的步骤。您将学习如何使用 D3 强大的图形计算在 SVG 画布上放置组件,以及如何将自己的图形操作与 D3 的布局相结合。我还将探讨如何使用 JavaScript 对象表示法 (JSON) 作为一种可用于可视化的数据格式。本文最后将展示如何使用布局组合在单个 SVG 画布上排列各种图形组件。

本文中的概念和示例以 第 1 部分中的概念和示例为基础,所以在继续阅读本文之前,请务必阅读或复习第 1 部分。阅读第 2 部分时,您可能会发现,在浏览器中并列打开两篇文章会很有用,这样您可参考第 1 部分中的图像。参见 下载,获取第 2 部分的样例代码。

D3 的图形布局简介

我以 第 1 部分中学到的 D3 功能为基础开始构建。

回想一下,第 1 部分中的图 1 和图 2 仅在圆圈排列上存在区别,这些图的 JavaScript 代码中的 transform属性值(如第 1 部分中的清单 2 和清单 4 所示)会计算每个圆圈中心的相对位置。

在 D3 的术语中,确定各个组件相对位置的图形计算被称为 布局。D3 提供了多种强大且可重用的布局。第 1 部分中的圆弧和弦的排列就是其中之一。知道如何单独使用 D3 的布局并能与自己的图形计算相结合,会很方便。

D3 包布局 (Pack layout)(如图 1 所示)是一个较大圆圈内的一个圆圈包。(与 第 1 部分中一样,这些圆圈描绘了第 1 周的页面查看数据。)在本文中,我将演示如何结合使用包布局和我自己的一些计算。

图 1. 在一个较大圆圈中显示圆圈的包布局
D3 包布局:一个较大圆圈中包含小圆圈

图 1中的圆圈与第 1 部分中生成的圆圈相同,但采用不同的排列方式。图形计算所导致的区别由 D3 的包布局完成。

没有外部的圆圈,图 1中的布局类似于图 2:

图 2. 没有外部圆圈的包布局
没有外部圆圈的包布局

第 1 部分的图 1 使用了一种基于我自己的简单图形计算的简单布局,而这里的 图 2使用了 D3 的包布局。如果将两个布局所完成的图形计算相结合,就可以直观地表示几周内的流行度数据,如图 3 所示:

图 3. 一个图形中包含三周的流行度数据
一个图形中包含三周的流行度数据

也可以将 图 3第 1 部分的图 3 中的嵌套圆圈排列相结合,在一个画布中一起描绘几周的流行度数据和用户互动数据,如图 4 所示:

图 4. 组合多个布局来显示几周的流行度数据和用户互动数据
组合多个布局来显示几周的流行度数据和用户互动数据

现在我将介绍显示图 1 到图 4 的 SVG 和基于 D3 的 JavaScript 代码。


绘制一个圆圈包

清单 1 给出了绘制 图 1中所示的圆圈包的 SVG 代码:

清单 1. 绘制图 1 中的圆圈包的 SVG 代码
 <?xml version=";1.0"; standalone=";no";?> 
 <!DOCTYPE svg PUBLIC ";-//W3C//DTD SVG 1.1//EN"; 
";http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd";> 

 <svg xmlns=";http://www.w3.org/2000/svg"; version=";1.1"; 
 width=";1000"; height=";1000";> 

 <g> 
   <circle 
      r=";250"; 
      style=";fill: #da70d6;"; 
      transform=";translate(250,250)";> 
   </circle> 
   <text 
      x=";-10"; 
      fill=";grey"; 
      transform=";translate(250,250)";> 
         26733 
   </text> 
 </g> 

 <g> 
   <circle 
      r=";91.28916405756645"; 
      style=";fill: #0000ff;"; 
      transform= 
       ";translate(172.869706621408,159.71405581800542)";> 
   </circle> 
   <text 
      x=";-10"; fill=";grey"; 
      transform= 
       ";translate(172.869706621408,159.71405581800542)";> 
            7057 
   </text></g> 

 <g> 
   <circle 
      r=";94.00415374552455"; 
      style=";fill: #ffd700;"; 
      transform= 
        ";translate(120.90301328980084,337.57095449403647)";> 
   </circle> 
   <text 
      x=";-10"; 
      fill=";grey"; 
      transform= 
       ";translate(120.90301328980084,337.57095449403647)";> 
            7483 
   </text> 
 </g> 

 <g> 
   <circle 
      r=";66.53756320431968"; 
      style=";fill: #008000;"; 
      transform= 
        ";translate(271.77788076862765,282.7036851574789)";> 
   </circle> 
   <text 
      x=";-10"; 
      fill=";grey"; 
      transform= 
        ";translate(271.77788076862765,282.7036851574789)";> 
            3749 
   </text> 
 </g> 

 <g> 
   <circle 
      r=";67.39284824138821"; 
      style=";fill: #ff0000;"; 
      transform= 
        ";translate(405.70829221433553,282.7036851574789)";> 
   </circle> 
   <text 
      x=";-10"; 
      fill=";grey"; 
      transform= 
        ";translate(405.70829221433553,282.7036851574789)";> 
            3846 
   </text> 
 </g> 

 <g> 
   <circle 
      r=";73.68747158608183"; 
      style=";fill: #800000;"; 
      transform= 
        ";translate(337.8448727920879,159.01774033373852)";> 
   </circle> 
   <text 
      x=";-10"; 
      fill=";grey"; 
      transform= 
        ";translate(337.8448727920879,159.01774033373852)";> 
            4598 
   </text> 
 </g> 

 </svg>

可以看到,清单 1中的根 <svg>标记包含 6 个 <g>子标记,每个子标记有一对 <circle><text>标记。第一个 <g>标记绘制了较大的外部圆圈,包含这个圆圈只是为了让您了解 D3 的包布局。在样例应用程序中并不需要外部圆圈,所以我通过将它的颜色从 da70d6(淡紫色)更改为 fffff(白色)而让它不可见,结果得到了 图 2而不是 图 1。剩余 5 个 <circle>标记中的每一个标记绘制了 图 2中 5 个圆圈中的一个圆圈,每个圆圈代表一种社会资源的流行度。

关于这 5 个 <circle>标记,有两点值得注意:

  • 每个 <circle>标记的 r属性(每个圆圈的半径)表示该圆圈的社会资源的流行度(资源越流行,圆圈越大)。
  • transform属性的 translate(x,y)值将每个圆圈放在这个包中。

rxy值完全决定了每个圆圈的大小和位置。D3 的包布局为您计算了这三个属性。清单 2 给出了使用 D3 执行图形计算并生成 清单 1中的 SVG 的 JavaScript 代码:

清单 2. 生成圆圈的包布局的基于 D3 的 JavaScript 代码
 <!DOCTYPE html> 
 <meta charset="utf-8"> 
 <body> 
 <!-- 
 <script src="https://github.com/douglascrockford/JSON-js/blob/master/json2.js"></script> 
 --> 
 <script src="d3.v3.js"></script> 
 <!--Save d3.v3.js in the same folder--> 
 <script> 

 var views = [ 
  [7057, 7483, 3749, 3846, 4598], 
  [ 2371, 7397, 4589, 2861, 8249], 
  [ 5972, 5672, 9152, 9725, 8983], 
  [ 9763,  8462,  9782, 1953, 5182], 
  [ 9567,  1571,  2895, 2783, 1874], 
  [ 2371, 7397, 4589, 2861, 8249] 
		 ]; 
 var width = 1000, height = 1000; 
 var colors = [ 
 "white",//"orchid", if you want to see the outer bigger circle. 
            "blue", 
            "gold", 
            "green", 
            "red", 
            "maroon"
		 ]; 
 var week = 0; 

 //Step 1: Make JSON representation of popularity data.
 var viewsJSON = getJSONForOneWeekOfPopularityData(0); 

 //Step 2: Pass on JSON to pack layout and get graphical 
 //calculations in return.
 var pack = d3.layout.pack() 
    .size([500, 500]) 
    .value(function(d) { return d.popularity; }); 
 var packCalculations = pack.nodes(viewsJSON); 

 //Step 3: Use calculations to generate SVG.
 var svg = d3.select("body").append("svg") 

 .attr("width", width) 
   .attr("height", height) 

   .selectAll("g").data(packCalculations).enter(); 
 var g = svg.append("g"); 
 g.append("circle") 
  .attr("r", function(d){return d.r}) 
  .style("fill", function(d, i){return colors[i%6];}) 
  .attr("transform", function(d,i){return "translate(" + d.x + "," + d.y + ")"}); 
 g.append("text") 
  .attr("x", -10) 
  .attr("fill", "grey") 
  .text(function(d){return d.value}) 
  .attr("transform", function(d,i){return "translate(" + d.x + "," + d.y + ")"}); 

 function getJSONForOneWeekOfPopularityData(week){ 
 var viewsJSONString = 
  "{ \"name\": \"week" + week + "\", \"children\": [" ; 
  for (var count=0; 
           count<5; 
           count++   ){ 
  viewsJSONString += 
    "{ \"name\": \"" + colors[count+1] + " \", \"popularity\": " 
    + views[week][count] + " }   "; 

  if (count<4) viewsJSONString += ","; 

  } 
  viewsJSONString += "]}"
  return JSON.parse(viewsJSONString); 
 } 
 </script> 
 </body>

可在 清单 2中看到三个步骤:

  1. 创建流行度的一种是用 JSON 表示,D3 的包布局可使用它执行图形计算。
  2. 将该 JSON 对象传递到包布局并返回图形计算结果。
  3. 使用计算结果生成所需的 SVG 代码。

下面介绍以下每个步骤。

第 1 步:使用 JSON

包布局是一种分层布局:它以在较大的(父)圆圈内包装较小的(子)圆圈形式表示分层数据。(D3 没有对分层级别的数量设置限制;您可以有父圆圈、祖父圆圈、曾祖父圆圈等。)示例的流行度和用户交互数据只有两个级别:父级别是第几周,子级别是每一周的流行度和用户互动数据。

包布局不直接处理 JavaScript 数组。它接受数据的一种 JSON 表示作为输入。JSON 数据格式可以很好地表示分层数据,而且受现代浏览器的直接支持。

清单 3 显示了流行度数据的一种 JSON 表示与相同数据的一种数组表示:

清单 3. 三周的流行度数据的数组和 JSON 表示
 //array representation 
 var views = [ 
  [7057, 7483, 3749, 3846, 4598], 
  [ 2371, 7397, 4589, 2861, 8249], 
  [ 5972, 5672, 9152, 9725, 8983] 
                   ]; 

 //JSON representation 
 var viewsJSON = 
 { 
  "name": "PopularityData", 
  "children": [ 
  { 
    "name": "week1", 

    "children": [ 
      {"name": "blue", "popularity": 7057}, 
      {"name": "gold", "popularity": 7483}, 
      {"name": "green", "popularity": 3749}, 
      {"name": "red", "popularity": 3846}, 
      {"name": "maroon", "popularity": 4598} 
    ] 
  }, 
  { 
    "name": "week2", 
    "children": [ 
      {"name": "blue", "popularity": 2371}, 
      {"name": "gold", "popularity": 7397}, 
      {"name": "green", "popularity": 4589}, 
      {"name": "red", "popularity": 2861}, 
      {"name": "maroon", "popularity": 8249} 
    ] 
  } 
  { 
    "name": "week3", 
    "children": [ 
      {"name": "blue", "popularity": 5972}, 
      {"name": "gold", "popularity": 5672}, 
      {"name": "green", "popularity": 9152}, 
      {"name": "red", "popularity": 9725}, 
      {"name": "maroon", "popularity": 8983} 
    ] 
  } 
 ]};

请注意,JSON 表示使用名称 - 值对。整个数据对象的名称是 PopularityDataPopularityData有三个子对象,它们分别名为 Week1Week2Week3。而且每个子对象有 5 个根据社交资源命名的子对象。(社交资源的名称是颜色名称,回想 第 1 部分您就会知道。)所有这些对象都被称为 节点;没有子节点的节点(也就是树分层结构的末端)被称为 叶节点(leaf node)。流行度数据是每个叶节点的 popularity属性的值。

清单 4 展示了如何为一周的流行度数据创建一个 JSON 对象:

清单 4. JSON 格式的一周的流行度数据
 var viewsJSON = 
 { 

    "name": "week1", 
    "children": [ 
      {"name": "blue", "popularity": 7057}, 
      {"name": "gold", "popularity": 7483}, 
      {"name": "green", "popularity": 3749}, 
      {"name": "red", "popularity": 3846}, 
      {"name": "maroon", "popularity": 4598} 
  ] 
 };

清单 5 展示了如何向 清单 3的 JSON 对象中的流行度数据添加用户互动:

清单 5. JSON 格式的三周流行度数据和用户互动数据
 var viewsJSON = 
 { 
  "name": "PopularityData", 
  "children": [ 
  { 
    "name": "week1", 
    "children": [ 
      {"name": "blue", "popularity": 7057, 
                       "user-interaction": 2052}, 
      {"name": "gold", "popularity": 7483}, 
                       "user-interaction": 2089}, 
      {"name": "green", "popularity": 3749}, 
                       "user-interaction": 1586}, 
      {"name": "red", "popularity": 3846}, 
                       "user-interaction": 1426}, 
      {"name": "maroon", "popularity": 4598} 
                       "user-interaction": 2632}, 
    ] 
  }, 
  { 
    "name": "week2", 
    "children": [ 
      {"name": "blue", "popularity": 2371}, 
                       "user-interaction": 2071}, 
      {"name": "gold", "popularity": 7397}, 
                       "user-interaction": 2190}, 
      {"name": "green", "popularity": 4589}, 
                       "user-interaction": 7214}, 
      {"name": "red", "popularity": 2861}, 
                       "user-interaction": 3782}, 
      {"name": "maroon", "popularity": 8249} 
                       "user-interaction": 2721}, 
    ] 
  } 
  { 
    "name": "week3", 
    "children": [ 
      {"name": "blue", "popularity": 5972}, 
                       "user-interaction": 3076}, 
      {"name": "gold", "popularity": 5672}, 
                       "user-interaction": 3190}, 
      {"name": "green", "popularity": 9152}, 
                       "user-interaction": 4532}, 
      {"name": "red", "popularity": 9725}, 
                       "user-interaction": 3825}, 
      {"name": "maroon", "popularity": 8983} 
                       "user-interaction": 4831}, 
    ] 
  } 
 ]};

清单 5可以看到,要添加用户互动数据,只需向每个叶节点添加一个名为 user-interaction的属性。

可以在 JavaScript 中以多种方式使用 JSON 数据。一种方式是将它直接包含在 JavaScript 文件中。另一种方式是通过 <script>标记将 JSON 文件(具有 .json 扩展名)链接到您的页面。或者可以拥有另一种格式的数据(比如 JavaScript 数组),它可在运行时动态地转换为 JSON。

将外部 JSON 文件与 JavaScript 页面相链接可能产生安全问题。相比较而言,在服务器端创建 JavaScript 文件时,数组提供了一种轻松而又明确的方式来存储数据。因此,我选择第 3 个选项:将数据存储在 JavaScript 数组中,并在同一个 JavaScript 文件中使用 “数组到 JSON” 转换逻辑。使用此选项,您可在需要数据的 JSON 表示时调用 “数组到 JSON” 转换逻辑。

您可能已经猜到,要绘制 图 1中的布局,需要一周流行度数据的 清单 4所示格式的 JSON 表示。名为 getJSONForOneWeekOfPopularityData()的简单 JavaScript 函数(如清单 6 所示)以这种格式生成了一个 JSON 对象:

清单 6. 从 JavaScript 数组生成一个 JSON 对象
 function getJSONForOneWeekOfPopularityData(week){ 

 //Step 1: Author a string to JSON format. 
 var viewsJSONString = 
  "{ \"name\": \"week" + week + "\", \"children\": [" ; 
  for (var count=0; 
           count<5; 
           count++   ){ 
  viewsJSONString += 
    "{ \"name\": \"" + colors[count+1] + " \", \"popularity\": " 
    + views[week][count] + " }   "; 

  if (count<4) viewsJSONString += ","; 

  } 
  viewsJSONString += "]}"

 //Step 2: Parse the string to create a JSON object. 
  return JSON.parse(viewsJSONString); 
 } 
 </script> 
 </body>

清单 6中可以看到,生成一个 JSON 对象是一个简单的两步转换过程。首先,创建一个与您想要生成的 JSON 格式完全一致的 JavaScript String表示。在创作字符串表示时,应该使用社交资源的实际名称,以便让它看起来类似于 清单 4。在第二步中,使用 JSON.parse()方法将该字符串转换为一个 JSON 对象。大多数现代浏览器都支持与 JSON 相关的功能,所以您可直接调用 JSON.parse()函数。我使用 Chrome Version 26.0.1410.64 m 试验过本文的代码。如果您的应用程序以旧版浏览器为目标,那么您可以通过一个 <script>标记在页面中包含一个 https://github.com/douglascrockford/JSON-js/blob/master/json2.js 库(它实现了 JSON 功能)。

第 2 步:使用包布局执行图形计算

现在有了合适的 JSON 对象,您可以将它传递到 D3 的包布局来执行所需的图形计算。清单 7(为了简便起见,它重复了 清单 2中的第 2 步)表明从包布局抓取图形计算结果有多么简单:

清单 7. 使用 D3 在一个圆圈包中为一周的流行度数据执行图形计算
//Step 2: Pass on JSON to pack layout and get graphical 
 //calculations in return.
 var pack = d3.layout.pack() 
    .size([500, 500]) 
    .value(function(d) { return d.popularity; }); 
 var packCalculations = pack.nodes(viewsJSON);

您只需创建一个 d3.layout.pack()对象,然后调用 size()函数设置包布局的大小。size()函数接受长度和宽度作为参数,这设置了用来绘制圆圈包的尺寸。D3 在内部依据布局的大小而缩放每个圆圈的大小。

接下来,要为包布局对象调用 value()函数,并写入您自己的函数作为值。这个函数为每个 JSON 节点返回一个值。在它执行图形计算时,D3 在内部调用此函数,对每个节点调用一次,将该节点被作为 d参数传递。在 清单 7中可以看到,我返回 d.popularity作为一个节点的值,因此告诉 D3 执行基于流行度数据的图形计算。结果,包中的每个圆圈依据节点的流行度数据而调整大小。

包布局对象现在已准备好接受 JSON 对象并执行计算。接下来,我调用布局对象的 nodes()函数,将 JSON 对象传递给该函数调用。

nodes()函数执行所有图形计算,以便将流行度圆圈放在包布局中。计算的结果为节点数组格式,每个节点表示大小并绘制一个圆圈的位置。整个节点数组包含绘制整个流行度圆圈包的所有数据。

简单来讲:您将 JavaScript 数组转换为 JSON 并将该 JSON 传递给 D3 的包布局。包布局执行图形计算并将计算结果放在另一个数组中(这一次是一个节点数组),在 清单 7中,该数组是一个名为 packCalculations的数组。

对于 packCalculations数组中的每个节点,D3 提供了多个属性,包括 rxy。现在您将看到如何使用这些属性创作 清单 1中的 SVG 代码。

第 3 步:使用包布局计算结果来生成 SVG

第 2 步完成后,packCalculations数组拥有绘制圆圈包所需的所有图形数据。第 3 步(如清单 8 中所述)使用计算结果生成 SVG:

清单 8. 使用图形计算结果绘制圆圈
//Step 3: Use calculations to generate SVG.
 var svg = d3.select("body").append("svg") 

 .attr("width", width) 
   .attr("height", height) 
   .selectAll("g").data(packCalculations).enter(); 
 var g = svg.append("g"); 
 g.append("circle") 
  .attr("r", function(d){return d.r}) 
  .style("fill", function(d, i){return colors[i%6];}) 
  .attr("transform", function(d,i) 
       {return "translate(" + d.x+ "," + d.y+ ")"}); 
 g.append("text") 
  .attr("x", -10) 
  .attr("fill", "grey") 
  .text(function(d){return d.value}) 
  .attr("transform", function(d,i) 
         {return "translate(" + d.x + "," + d.y + ")"});

您在 清单 8中看到的大部分代码都来自 第 1 部分。惟一的区别是,这一次数据来自 packCalculations数组。不要担忧如何获取数组中的每个节点并逐个绘制每个圆圈。可将 packCalculations数组传递给富有魔力的 .data(packCalculations)D3 函数,它会处理大部分工作。

请注意,清单 8中使用了 d.rd.xd.y属性。要调整和放置每个圆圈,需要使用这些 rxy属性。d.r属性用于创作圆圈的 r属性(半径),而 d.xd.y属性用于创作放置每个圆圈的 transform属性的值。

如果运行 清单 2中的 JavaScript 代码,就会看到 图 2中所示的图像。


将外部计算与包布局相结合

下面介绍了如何将包布局与您自己的图形计算相结合,以绘制 图 3中的图形。您可以看到,图 3对圆圈的包布局使用了三次(一次针对一周),得到了三组圆圈(每组 5 个),一组圆圈表示一周的流行度数据。

清单 9 显示了 图 3的 SVG 代码:

清单 9. 显示三周流行度数据的 SVG 代码
 <?xml version="1.0" standalone="no"?> 
 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" 
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 

 <svg xmlns="http://www.w3.org/2000/svg" version="1.1"
 width="1000" height="1000"> 

 <g transform="translate(0,143)"> 
  <text x="60">Week 1</text> 
  <!-- resource-popularity tags omitted.--> 
 </g> 

 <g transform="translate(320,143)"> 
  <text x="60">Week 2</text> 
  <!-- resource-popularity tags omitted.--> 
 </g> 

 <g transform="translate(640,143)"> 
  <text x="60">Week 3</text> 
  <!-- resource-popularity tags omitted.--> 
 </g> 

 </svg>

清单 9显示了根 <svg>标记的三个 <g>子标记。我将这三个 <g>标记称为 周包装器标记,因为每一个标记表示一周的数据。

每个周包装器标记有 6 个 <g>子标记(一个较大的圆圈加上 5 个流行度圆圈);我将它们称为 资源流行度标记。我省略了资源流行度标记,因为在 清单 1中,您已知道它们是什么样子。

现在看看周包装器(week-wrapper)标记的 transform属性。这些值将第一个圆圈包放在点 0, 143;将第二个放在 320, 143;将第三个放在 640, 143。这种放置是我自己的简单图形计算的结果,它包装了所有圆圈包。下面将介绍我是如何做的。

生成清单 9 中的 SVG 代码的 JavaScript

为了生成 清单 9中的 SVG,清单 10 中的 JavaScript 代码将 D3 的包布局与我自己的一些图形计算相结合:

清单 10. 将包布局与更多图形计算相结合的 JavaScript
 <!DOCTYPE html> 

 <meta charset="utf-8"> 
 <body> 
 <!-- 
 <script src="https://github.com/douglascrockford/JSON-js/blob/master/json2.js"></script> 
 --> 
 <script src="d3.v3.js"></script> 
 <script> 

 var views = [ 
  [7057, 7483, 3749, 3846, 4598], 
  [ 2371, 7397, 4589, 2861, 8249], 
  [ 5972, 5672, 9152, 9725, 8983], 
		 ]; 


 var colors = [ 
  "white", 
  "blue", 
  "gold", 
  "green", 
  "red", 
  "maroon"   ]; 

 var width = 1500, height = 1000; 
 var widthOfOnePack = 300, heightOfOnePack = 300; 
 var spaceBetweenPacks = 20; 
 var packCalculations = []; 
 for (var count=0; count<3; count++){ 
  var pack = d3.layout.pack() 
              .size([widthOfOnePack, heightOfOnePack]) 
              .value(function(d) { 
                 return d.popularity; }); 
  packCalculations[count] = 
     pack.nodes( 
       getJSONForOneWeekOfPopularityData(count) 
               ); 
 }
 var svg = d3.select("body").append("svg") 
   .attr("width", width) 
   .attr("height", height); 
 var g1 = svg.selectAll("g") 
          .data(packCalculations).enter().append("g") 
    .attr("transform", function(d,i){ 
             return "translate(" + 
            (widthOfOnePack + spaceBetweenPacks )*i + 
            "," + height / 7 + ")" });
 g1.append("text") 
 .text(function(d, i){return "Week " + (i+1)}) 
  .attr("x", 60); 

 var g2 = g1.selectAll("g") 
   .data(function(d){return d;}) 
   .enter() 
   .append("g"); 

 g2.append("circle") 
  .attr("r", function(d, i){return d.r}) 
  .style("fill", function(d, i){return colors[i%6];}) 
  .attr("transform", function(d,i){ 
         return "translate(" + d.x + "," + d.y + ")"}); 

 g2.append("text") 
  .attr("x", -10) 
  .attr("fill", "grey") 
  .text(function(d, i){return d.value}) 
  .attr("transform", function(d,i){ 
        return "translate(" + d.x + "," + d.y + ")"}); 

 function getJSONForOneWeekOfPopularityData(week){ 
 var viewsJSONString = 
  "{ \"name\": \"week" + week + "\", \"children\": [" ; 
  for (var count=0; 
           count<5; 
           count++   ){ 
  viewsJSONString += 
    "{ \"name\": \"" + colors[count+1] + " \", \"popularity\": " 
    + views[week][count] + " }   "; 

  if (count<4) viewsJSONString += ","; 

  } 
  viewsJSONString += "]}"
  return JSON.parse(viewsJSONString); 
 } 

 </script> 
 </body>

我突出显示了 清单 10中的 packCalculationstransform增强,以帮助您将此代码与 清单 2进行对比。

清单 2中,我将 packCalculations声明为一个简单变量,而在 清单 10中,我将它声明为一个数组。这是因为 清单 10处理三个圆圈包,每个包都拥有自己的图形计算。

声明 packCalculations数组后,我运行了一个简单循环,在其中使用 D3 的包布局执行在包中放置每个圆圈的计算。D3 返回每周的一个节点数组。我将 D3 的每组计算结果存储在 packCalculations数组中。所以 packCalculations现在是一个节点数组的数组。

在这里应当注意一个有趣的地方。D3 的所有计算都是为一个包执行的,与其他包独立,就像只需要处理一个包。这些计算没有考虑到其他包可能位于同一个 SVG 画布上的情况。但是当 transform属性转换一个周包装器标记的位置时,周包装器中的所有对象都是相对于包装器而进行放置的,所以您可以轻松地放置多个圆圈包。

现在看看计算周包装器标记的 transform属性值的计算(已在 清单 10中以粗体形式显示)。我将一个包的宽度加上一些空白再乘以索引 i。执行此调整后,放置第一个周包装器 (i=0) 时不需要执行任何水平位移。第二个周包装器 (i=1) 水平位移第一个周包装器的宽度加上少量空白。类似地,第三个包装器移位前两个周包装器的宽度加上额外的空白。

清单 10的剩余部分与 清单 2相同,您将在其中绘制每周的各个圆圈。显然,您可以:

  • 通过设计 <g>标记的父子排列来设置布局。
  • 使用每个 <g>标记的 transform属性值将各个组件或完整的组件布局放在想要放置的位置。

嵌套圆圈的一种包布局

下面介绍您可以对 清单 10执行的一些有趣增强,可以用它们在 图 3的布局中添加用户互动数据,从而形成 图 4中所示的嵌套圆圈包。

清单 11 给出了绘制 图 4的 JavaScript:

清单 11. 绘制图 4 的 JavaScript
 <!DOCTYPE html> 
 <meta charset="utf-8"> 
 <body> 
 <!-- 
 <script src="https://github.com/douglascrockford/JSON-js/blob/master/json2.js"></script> 
 --> 
 <script src="d3.v3.js"></script> 

 <script> 

 var viewsAndInteraction = [ 
 [ [7057, 2052], [7483, 2089], [3749, 1586], [3846, 1426], [4598, 2632] ], 
 [ [5972, 2071], [5672, 2190], [9152, 7214], [9725, 3782], [8983, 2721] ], 
 [ [8749, 3076], [4768, 3190], [6738, 4532], [9546, 3825], [6983, 4831] ] 
               ]; 

 var viewColors = [ "white", "blue", "gold", "green", "red", "maroon" ]; 
 var interactionColors = [ "white", "lightblue", "yellow", "lightgreen", 
   "lightcoral", "indianred" ]; 
 var width = 1500, height = 1000; 
 var widthOfOnePack = 300, heightOfOnePack = 300; 
 var spaceBetweenPacks = 20; 
 var packCalculations = []; 
  for (var count=0; 
           count<3; 
           count++   ){ 
    var pack = d3.layout.pack() 
    .size([widthOfOnePack, heightOfOnePack]) 
    .value(function(d) { return d.size; }); 
    packCalculations[count] = pack.nodes( 
    getJSONForOneWeekOfPopularityAndInteractionData( 
                  count)); 
 } 
 var svg = d3.select("body").append("svg") 
   .attr("width", width) 
   .attr("height", height); 
 var g1 = svg.selectAll("g") 
         .data(packCalculations).enter().append("g") 
         .attr("transform", function(d,i){ 
         return "translate(" + 
           (widthOfOnePack + spaceBetweenPacks)*i + "," + 
              height / 7 + ")" } 
         ); 
 g1.append("text") 
 .text(function(d, i){return "Week " + (i+1)}) 
  .attr("x", 60); 
 var g2 = g1.selectAll("g") 
   .data(function(d){return d;}) 
   .enter() 
   .append("g"); 

 g2.append("circle") 
  .attr("r", function(d, i){return d.r}) 
  .style("fill", function(d, i){return viewColors[i%6];}) 
  .attr("transform", function(d,i){ 
         return "translate(" + d.x + "," + d.y + ")"}); 

 g2.append("circle") 
  .attr("r", function(d, i){ 
        return ((d.interaction*d.r)/d.value);})
  .style("fill", function(d, i){return interactionColors[i%6];}) 
  .attr("transform", function(d,i){ 
         return "translate(" + d.x + "," + d.y + ")"}); 

 g2.append("text") 
  .attr("x", -15) 
  .attr("y", 35) 
  .attr("fill", "white") 

  .text(function(d, i){return d.value}) 
  .attr("transform", function(d,i){ 
       return "translate(" + d.x + "," + d.y + ")"}); 

 g2.append("text") 
  .attr("x", -15) 
  .attr("y", 5) 

  //.attr("fill", "grey") 
  .text(function(d, i){return d.interaction}) 
  .attr("transform", function(d,i){ 
       return "translate(" + d.x + "," + d.y + ")"}); 

 function getJSONForOneWeekOfPopularityAndInteractionData(week){ 
 var viewsAndInteractionJSON = 
  "{ \"name\": \"week" + week + "\", \"children\": [" ; 
  for (var count=0; count<5; count++){ 
    viewsAndInteractionJSON += 
     "{ \"name\": \"" + viewColors[count+1] + 
      " \", \"size\": \"" + viewsAndInteraction[week][count][0] +       
      "\", \"interaction\": \"" + 
      viewsAndInteraction[week][count][1] + "\"     }   "; 
  if (count<4) viewsAndInteractionJSON += ","; 
  } 
  viewsAndInteractionJSON += "]}"; 
  return JSON.parse(viewsAndInteractionJSON) ; 
 }

 </script> 
 </body>

清单 11中的增强(以粗体显示)包括:

  • “数组到 JSON” 转换逻辑更加复杂,因为这一次您必须生成 清单 5中的 JSON 代码,其中每个叶节点有两部分数据:流行度和用户互动。
  • 包布局对象的 value()函数仍然使用 d.popularity属性,并忽略了 user-interaction属性。包布局的图形计算仍然基于流行度数据。用户互动数据仅用于绘制外部流行度圆圈中的内部圆圈。请注意内部用户互动圆圈的 r属性值(半径)的计算,它在 清单 11中以粗体显示。这是用户互动数据发挥作用的地方。为了计算内部圆圈的半径,我将用户互动数据乘以外部(流行度)圆圈的半径与实际流行度数据的比率。这种细微的图形操作会根据 D3 用来执行它自己的内部计算的比例来缩放内部圆圈。

流行度和用户互动数据的条形图表示

我讨论了如何在不同布局中使用圆圈表示社交数据。现在我添加一点花样。例如,除了表示相同的社交数据的圆圈之外,图 5 还显示了条形图:

图 5. 社交数据的条形图表示
社交数据的条形图表示

您可以看到,图 5中的条形图也表示相同的三周流行度和用户互动数据。这些竖条使用我绘制内部和外部圆圈所用的相同颜色和阴影。每个竖条的实心部分表示流行度,浅色阴影部分表示用户互动。

清单 12 显示了绘制 图 5中的条形图的 SVG 代码(省略了圆圈的代码):

清单 12. 条形图的 SVG
 <?xml version="1.0" standalone="no"?> 
 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" 
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> 

 <svg xmlns="http://www.w3.org/2000/svg" version="1.1"
 width="1500" height="1000"> 

 <g> 
 <!--Tags for circles omitted.--> 
 </g> 

 <g> 

 <g transform="translate(100,250)"> 

  <g transform="translate(0,200)"> 
    <rect height="141.14" width="20" fill="blue"></rect> 
    <rect height="41.04" width="20" fill="lightblue"></rect> 
  </g> 

  <g transform="translate(22,200)"> 
    <rect height="149.66" width="20" fill="gold"></rect> 
    <rect height="41.78" width="20" fill="yellow"></rect> 
  </g> 

  <g transform="translate(44,200)"> 
    <rect height="74.98" width="20" fill="green"></rect> 
    <rect height="31.72" width="20" fill="lightgreen"></rect> 
  </g> 

  <g transform="translate(66,200)"> 
    <rect height="76.92" width="20" fill="red"></rect> 
    <rect height="28.52" width="20" fill="lightcoral"></rect> 
  </g> 

  <g transform="translate(88,200)"> 
    <rect height="91.96" width="20" fill="maroon"></rect> 
    <rect height="52.64" width="20" fill="indianred"></rect> 
  </g> 
 </g> 

 <g transform="translate(420,250)"> 

  <g transform="translate(0,200)"> 
    <rect height="119.44" width="20" fill="blue"></rect> 
    <rect height="41.42" width="20" fill="lightblue"></rect> 
  </g> 

  <g transform="translate(22,200)"> 
    <rect height="113.44" width="20" fill="gold"></rect> 
    <rect height="43.8" width="20" fill="yellow"></rect> 
  </g> 

  <g transform="translate(44,200)"> 
    <rect height="183.04" width="20" fill="green"></rect> 
    <rect height="144.28" width="20" fill="lightgreen"></rect> 
  </g> 

  <g transform="translate(66,200)"> 
    <rect height="194.5" width="20" fill="red"></rect> 
    <rect height="75.64" width="20" fill="lightcoral"></rect> 
  </g> 

  <g transform="translate(88,200)"> 
    <rect height="179.66" width="20" fill="maroon"></rect> 
   <rect height="54.42" width="20" fill="indianred"></rect> 
  </g> 
 </g> 

 <g transform="translate(740,250)"> 

  <g transform="translate(0,200)"> 
    <rect height="174.98" width="20" fill="blue"></rect> 
    <rect height="61.52" width="20" fill="lightblue"></rect> 
  </g> 

  <g transform="translate(22,200)"> 
    <rect height="95.36" width="20" fill="gold"></rect> 
    <rect height="63.8" width="20" fill="yellow"></rect> 
  </g> 

  <g transform="translate(44,200)"> 
    <rect height="134.76" width="20" fill="green"></rect> 
    <rect height="90.64" width="20" fill="lightgreen"></rect> 
  </g> 

  <g transform="translate(66,200)"> 
    <rect height="190.92" width="20" fill="red"></rect> 
    <rect height="76.5" width="20" fill="lightcoral"></rect> 
  </g> 

  <g transform="translate(88,200)"> 
    <rect height="139.66" width="20" fill="maroon"></rect> 
    <rect height="96.62" width="20" fill="indianred"></rect> 
  </g> 
 </g> 

 </g> 
 </svg>

您可以在 清单 12中看到熟悉的 <g>标记排列。每三个 <g>标记包装一周的数据。然后,每个周包装器标记有 5 个子 <g>标记,每个子标记包装一对 <rect>标记,用于表示实际的流行度和互动数据。一个 <rect>标记绘制一个矩形条,就像一个 <circle>标记绘制一个圆圈一样。

每个 <rect>标记有 widthheight属性。width属性值对所有 <rect>标记是固定的,height属性值与矩形条所表示的社交数据呈正比。竖条的固定宽度和可变高度带来了条形图的印象。

同样地,<g>标记的 transform属性将每个竖条放在正确的位置。换言之,圆圈和竖条的 SVG 代码之间没有根本区别。

生成 图 5中的条形图的基于 D3 的 JavaScript 代码包含在本文的源代码 下载中。

添加导航数据

您常常需要在一个页面上显示各种图形组件,所以我最后将所有内容一起放在一个 SVG 画布上。图 6 使用您在 第 1 部分中看到的弦图添加了三周的导航数据:

图 6. 单个图形中的圆圈、圆弧、弦、布局和条形图
单个图形中的圆圈、圆弧、弦、布局和条形图

图 6的 JavaScript 也包含在源代码 下载中。


结束语

在这个篇幅不长的文章系列中,我们经历了一次漫长的数据可视化之旅,从 第 1 部分的 图 1 到本文中的复杂安排。我希望这些文章能够激发您了解 D3 的其他特性的兴趣,这些特性通过探索 D3 网站可以找到。

D3 的一大优势在于它是基于 JavaScript 的,而 JavaScript 是顶尖业务应用程序的首选前端技术。如果您使用的是一个支持 JavaScript 的业务应用程序平台,比如 IBM Business Process Manager,那么您可以根据从此文章系列中学到的知识来构建图形特性,将它们应用于您的业务应用程序中。要了解 IBM Business Process Manager 及其对外部 JSON 和 JavaScript 库的支持,请参阅 参考资料,获取到达 IBM Business Process Manager Information Center 的链接。


下载

描述名字大小
样例代码source-code.zip10KB

参考资料

学习

获得产品和技术

  • D3:下载 D3 JavaScript 库。
  • IBM InfoSphere Streams:下载 InfoSphere Streams 并构建应用程序来快速获取、分析和关联来自数千个实时来源的信息。
  • IBM InfoSphere BigInsights:下载 IBM InfoSphere BigInsights 的试用版,管理和分析海量静止的结构化和非结构化数据。

讨论

  • 加入 developerWorks 社区。探索由开发人员推动的博客、论坛、群组和维基,并与其他 developerWorks 用户进行交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source, Web development, Information Management
ArticleID=946012
ArticleTitle=数据可视化,第 2 部分: 使用 D3 组件进行布局
publish-date=09232013