堆栈图布局(Stack Layout)能够计算二维数组每一数据层的基线,以方便将各数据层叠加起来。本文讲解堆栈图的制作方法。

先说说什么是堆栈图。

例如,有如下情况:

某公司,销售三种产品:个人电脑、智能手机、软件。

2005年,三种产品的利润分别为3000、2000、1100万。

2006年,三种产品的利润分别为1300、4000、1700万。

计算可得,2005年总利润为6100万,2006年为7000万。

如果要将2005年的利润用柱形表示,那么应该画三个矩形,三个矩形堆叠在一起。这时候就有一个问题:每一个矩形的起始y坐标是多少,高应该是多少。

输入数组,直接计算出上述问题的,就是堆栈图布局。

1. 数据

某公司销售个人电脑、智能手机、软件的数据如下:

  1. var dataset = [
  2. { name: "PC" ,
  3. sales: [ { year:2005, profit: 3000 },
  4. { year:2006, profit: 1300 },
  5. { year:2007, profit: 3700 },
  6. { year:2008, profit: 4900 },
  7. { year:2009, profit: 700 }] },
  8. { name: "SmartPhone" ,
  9. sales: [ { year:2005, profit: 2000 },
  10. { year:2006, profit: 4000 },
  11. { year:2007, profit: 1810 },
  12. { year:2008, profit: 6540 },
  13. { year:2009, profit: 2820 }] },
  14. { name: "Software" ,
  15. sales: [ { year:2005, profit: 1100 },
  16. { year:2006, profit: 1700 },
  17. { year:2007, profit: 1680 },
  18. { year:2008, profit: 4000 },
  19. { year:2009, profit: 4900 }] }
  20. ];

dataset是一个数组,数组的每一项是一个对象,对象里含有name和sales。name是产品名、sales是销售情况。sales也是一个数组,每一项也是对象,对象里包含有year表示年份、profit表示利润。

现要将此数据绘制成堆栈图。

2. 布局(数据转换)

首先创建堆栈图布局。

  1. var stack = d3.layout.stack()
  2. .values(function(d){ return d.sales; })
  3. .x(function(d){ return d.year; })
  4. .y(function(d){ return d.profit; });

values访问器指定的是d.sales,表示接下来接收的数组,要计算的数据在数组每一项的变量sales中。x访问器指定的是d.year,y访问器指定的是d.profit,都是相对于values访问器指定的对象说的,即sales数组每一项的变量year和profit。

以dataset为stack参数,结果保存在data中:

  1. var data = stack(dataset);
  2. console.log(data);

要注意,转换之后原数据也会改变,因此dataset和data的值是一样的。data的输出值如图1所示。可以看到,sales的每一项都多了两个值:y0和y。y0即该层起始坐标,y是高度。x坐标有就是year。

图1

3. 绘制

首先要创建x轴和y轴比例尺,在添加图形元素和坐标轴的时候都要用到。要绘制坐标轴,要立刻想到要给坐标轴的刻度留出一部分空白。先定义一个外边框对象。

  1. var padding = { left:50, right:100, top:30, bottom:30 };

右边部分留出的空白较多,是为了在后面添加标签的。x轴比例尺的定义如下:

  1. var xRangeWidth = width - padding.left - padding.right;
  2.  
  3. var xScale = d3.scale.ordinal()
  4. .domain( data[0].sales.map(function(d){
  5. return d.year;
  6. }))
  7. .rangeBands([0, xRangeWidth],0.3);

本例中x轴代表年份,2005年、2006年、2007年等,是离散的,也就是说比例尺的定义域是离散的。从第5章的内容可知,序数比例尺d3.scale.ordinal的定义域是离散的。上面代码将定义域设定成:

  1. [2005, 2006, 2007, 2008, 2009]

值域是根据rangeBands()计算的,实际是:

  1. [31, 134, 238, 342, 446] //(省略了小数点)

因此,在2005年处堆叠的矩形的x坐标为31。

然后再创建y轴的比例尺。

  1. //最大利润(定义域的最大值)
  2. var maxProfit = d3.max(data[data.length-1].sales, function(d){
  3. return d.y0 + d.y;
  4. });
  5.  
  6. //最大高度(值域的最大值)
  7. var yRangeWidth = height - padding.top - padding.bottom;
  8.  
  9. var yScale = d3.scale.linear()
  10. .domain([0, maxProfit]) //定义域
  11. .range([0, yRangeWidth]); //值域

这段代码中,求最大利润时,是对data数组中的最后一项data[2]求取sales的最大值。这是因为data[2]代表着最高的层,如图2所示,data[2].sales中的各项y0+y,必定比data[1]和data[0]的大。因此,只要用d3.max()求取data[2].sales中的最大值即可。值域是SVG的高度减去外边框的上下宽度。然后为d3.scale.linear()设定定义域和值域即可。

图2

有了比例尺后,需要添加足够数量的分组元素<g>,每一个分组代表一种产品,每一种产品都用一种颜色来标识。

  1. //颜色比例尺
  2. var color = d3.scale.category10();
  3.  
  4. //添加分组元素
  5. var groups = svg.selectAll("g")
  6. .data(data)
  7. .enter()
  8. .append("g")
  9. .style("fill",function(d,i){ return color(i); });

现在添加了3个分组,分别代表PC、SmartPhone、Software,且每一个分组元素的fill都设定了颜色。接下来为每个分组添加矩形元素。

  1. //添加矩形
  2. var rects = groups.selectAll("rect")
  3. .data(function(d){ return d.sales; })
  4. .enter()
  5. .append("rect")
  6. .attr("x",function(d){ return xScale(d.year); })
  7. .attr("y",function(d){
  8. return yRangeWidth - yScale( d.y0 + d.y );
  9. })
  10. .attr("width",function(d){
  11. return xScale.rangeBand();
  12. })
  13. .attr("height",function(d){ return yScale(d.y); })
  14. .attr("transform","translate(" + padding.left + ","
  15. + padding.top + ")");

每一个分组元素里还要绑定数组sales,以添加足够数量(每个分组5个)的矩形。然后再使用比例尺为矩形的x、y、width、height属性赋值。再添加上坐标轴后,结果如图3所示。

图3

但是,什么颜色代表什么产品从图里看不出来。解决此问题最常用的方法是,在图表旁边添加几个图形标志,旁边加上文字,告诉用户某种颜色对应的是什么。在分组里继续添加图形元素,代码如下。

  1. var labHeight = 50;
  2. var labRadius = 10;
  3.  
  4. var labelCircle = groups.append("circle")
  5. .attr("cx",function(d){
  6. return width - padding.right*0.98;
  7. })
  8. .attr("cy",function(d,i){
  9. return padding.top * 2 + labHeight * i;
  10. })
  11. .attr("r",labRadius);
  12.  
  13. var labelText = groups.append("text")
  14. .attr("x",function(d){
  15. return width - padding.right*0.8;
  16. })
  17. .attr("y",function(d,i){
  18. return padding.top * 2 + labHeight * i;
  19. })
  20. .attr("dy",labRadius/2)
  21. .text(function(d){ return d.name; });

用圆和文字作为标签,添加到图表的右边。最终结果如图4所示。

图4

完整代码请点击以下网址,再右键查看源代码:

http://www.ourd3js.com/demo/G-3.0/stack.html

谢谢阅读。

文档信息

【 D3.js 高级系列 — 3.0 】 堆栈图的更多相关文章

  1. 【 D3.js 高级系列 — 2.0 】 机械图 + 人物关系图

    机械图(力路线图)结合老百姓的关系图中的生活,这是更有趣. 本文将以此为证据,所列的如何图插入外部的图像和文字的力学. 在[第 9.2 章]中制作了一个最简单的力学图.其后有非常多朋友有疑问,基本的问 ...

  2. 【 D3.js 高级系列 — 10.0 】 思维导图

    思维导图的节点具有层级关系和隶属关系,很像枝叶从树干伸展开来的形状.在前面讲解布局的时候,提到有五个布局是由层级布局扩展来的,其中的树状图(tree layout)和集群图(cluster layou ...

  3. 【 D3.js 高级系列 — 2.0 】 捆图

    捆图(Bundle)是 D3 中比较奇特的一个布局,只有两个函数,而且需要与其它布局配合使用.本文讲述捆图的制作方法. 有关捆图的例子极少,很容易找到的是:http://bl.ocks.org/mbo ...

  4. 【 D3.js 高级系列 — 4.0 】 矩阵树图

    矩阵树图(Treemap),也是层级布局的扩展,根据数据将区域划分为矩形的集合.矩形的大小和颜色,都是数据的反映.许多门户网站都能见到类似图1,将照片以不同大小的矩形排列的情形,这正是矩阵树图的应用. ...

  5. 【 D3.js 高级系列 — 1.0 】 文本的换行

    在 SVG 中添加文本是使用 text 元素.但是,这个元素不能够自动换行,超出的部分就显示不出来了,怎么办呢? 高级系列开篇前言 从今天开始写高级系列教程.还是那句话,由于本人实力有限,不一定保证入 ...

  6. 【 D3.js 高级系列 — 9.0 】 交互式提示框

    一般来说,图表中不宜存在过多文字.但是,有时需要一些文字来描述某些图形元素.那么,可以实现一种交互:当用户鼠标滑到某图形元素时,出现一个提示框,里面写有描述文字.这是一种简单.普遍的交互式,几乎适用于 ...

  7. 【 D3.js 高级系列 — 6.0 】 值域和颜色

    在[入门 - 第 10 章]作了一张中国地图,其中各省份的颜色值都是随意赋值的.如果要将一些值反映在地图上,可以利用颜色的变化来表示值的变化. 1. 思路 例如,有值域的范围为: [10, 500] ...

  8. 【 D3.js 高级系列 — 8.0 】 标线

    有时候,需要在地图上绘制连线,表示"从某处到某处"的意思,这种时候在地图上绘制的连线,称为"标线". 1. 标线是什么 标线,是指地图上需要两个坐标以上才能表示 ...

  9. 【 D3.js 高级系列 — 7.0 】 标注地点

    有时需要告诉用户地图上的一些目标,如果该目标是只需要一个坐标就能表示的,称其为"标注". 1. 标注是什么 标注,是指地图上只需要一个坐标即可表示的元素.例如,在经纬度(116, ...

随机推荐

  1. C中浮点数转字符串

    求浮点数转换成字符串,如何才能获得比较正确的字符串.用printf("%f\n", (float)5); 这种方式转换出来的结果是 5.000000 ,末尾都会带6位小数. 控制精 ...

  2. 【NHibernate】HQL入门

    在NHibernate 中 HQL 可以帮我们转成最终依赖数据库的查询脚本: 语法也甚是强大,适配主流数据库, HQL不支持union,要想取多个表数据可以做两次单独查询. IQuery query ...

  3. 【学习总结】 init & initWithFrame & initWithCoder

    //当我们所写的程序里没用用Nib文件(XIB)时,用代码控制视图内容,需要调用initWithFrame去初始化 - (id)initWithFrame:(CGRect)frame { if (se ...

  4. 判断webpart类型 How can I tell what type a web part is?

    using(new SPSite("http://mysite/myweb").OpenWeb()){ //give relative path of the webpartpag ...

  5. csharp excel interop programming

    string fileName = "c:\\a.xlsx"; var application = new Application(); application.Visible = ...

  6. submit与button区别提交区别

    提交表单时使用submit会自动提交form表单数据, 如果使用jquery的form表单插件时需要将提交按钮改为button时$("#表单id").ajaxSubmit({}); ...

  7. python学习笔记24(路径与文件 (os.path包, glob包))

    os.path模块主要用于文件的属性获取,在编程中经常用到,以下是该模块的几种常用方法. >>> import os.path >>> path = '/home/ ...

  8. DevExpress GridControl 导出为Excel

    private void btnExport_ItemClick(object sender, EventArgs e)         {             SaveFileDialog sa ...

  9. python 中的列表解析和生成表达式 - 转

    优雅.清晰和务实都是python的核心价值观,如果想通过操作和处理一个序列(或其他的可迭代对象)来创建一个新的列表时可以使用列表解析(  List comprehensions)和生成表达式,通过这两 ...

  10. C++函数中那些不可以被声明为虚函数的函数

    转自C++函数中那些不可以被声明为虚函数的函数 常见的不不能声明为虚函数的有:普通函数(非成员函数):静态成员函数:内联成员函数:构造函数:友元函数. 1.为什么C++不支持普通函数为虚函数? 普通函 ...