JavaScript-图表入门指南-全-
JavaScript 图表入门指南(全)
一、绘图技术概述
Abstract
当我们需要用图形来表示数据或定性结构,以显示关系,进行比较或突出趋势时,我们会使用图表。图表是由符号组成的图形结构,如折线图中的线条;条形图中的条形;或称切片。图表是有效的工具,可以帮助我们辨别和理解大量数据背后的关系。对于人类来说,阅读图形表示(如图表)比阅读原始数字数据更容易。如今,在各种各样的专业领域以及日常生活的许多其他方面,使用图表已经成为惯例。由于这个原因,根据数据的结构和突出显示的现象,图表有多种形式。例如,如果您将数据分成不同的组,并希望表示每个组相对于总数的百分比,则通常在饼图或条形图中显示这些数据组。相比之下,如果您想要显示一个变量随时间变化的趋势,折线图通常是最佳选择。
当我们需要用图形来表示数据或定性结构,以显示关系,进行比较或突出趋势时,我们会使用图表。图表是由符号组成的图形结构,如折线图中的线条;条形图中的条形;或称切片。图表是有效的工具,可以帮助我们辨别和理解大量数据背后的关系。对于人类来说,阅读图形表示(如图表)比阅读原始数字数据更容易。如今,在各种各样的专业领域以及日常生活的许多其他方面,使用图表已经成为惯例。由于这个原因,根据数据的结构和突出显示的现象,图表有多种形式。例如,如果您将数据分成不同的组,并希望表示每个组相对于总数的百分比,则通常在饼图或条形图中显示这些数据组。相比之下,如果您想要显示一个变量随时间变化的趋势,折线图通常是最佳选择。
在本书中,您将学习如何使用基于 JavaScript 的各种技术来创建、绘制图表,并根据您的需求调整图表。然而,在开始使用 JavaScript 开发图表之前,理解本书章节中涉及的基本概念是很重要的。在这一章中,我将提供这些概念的简要概述。
首先,我将向您展示如何识别构成图表的最常见元素。了解这些元素将会很有帮助,因为您会发现它们以组件、变量和对象的形式存在于专门为实现图表而创建的 JavaScript 库中。
接下来,我将列出最常见的图表类型。您对图表及其功能的了解越多,就越容易为您的数据选择正确的表示方式。如果你要强调你想要表现的关系,做出正确的选择是很重要的,仅仅阅读数据是不够的。只有当你熟悉了最常见的图表类型,你才能选择最适合你的目的的图表。
一旦你熟悉了这些概念,你将需要学习如何通过网络实现它们,以及当前有哪些技术可以帮助你实现这个目标。因此,在这一章的第二部分,我将讨论这些技术方面,逐一介绍本书中提供的例子的开发中所涉及的技术。
最后,鉴于我们所有的工作都将集中在 JavaScript 代码的开发上,我认为对某些类型的数据提供一个简要的描述会有所帮助。那些不熟悉 JavaScript 的人可以从这个关于代码中数据形式的快速参考资料中受益。然而,我强烈建议读者更深入地研究本章中讨论的概念和技术。
图表中的元素
您很快就会看到,图表可以呈现多种形式。在图表中,通过使用特定于图表类型的符号,数据呈现出图形结构;但是,有一些特性是所有图表共有的。
通常,每个图表都有一个标题,显示在顶部,提供数据的简短描述。不太常见的是,副标题或脚注用于提供额外的描述(主要是与数据相关的信息,如参考文献、地点、日期和注释)。
图表通常有轴——两条垂直线,允许用户引用每个数据点 P(x,y)的坐标值(x,y),如图 1-1 所示。水平线通常代表 x 轴,垂直线代表 y 轴。
图 1-1。
A two-dimensional chart
每个轴上都定义了一个刻度。尺度可以是数字的,也可以是分类的。每个轴被分成对应于刻度所代表的特定值范围的段。一个线段和下一个线段之间的边界称为记号。每个分笔成交点报告与该轴相关的刻度值。一般来说,称这些为刻度标签。
图 1-2 显示了四个不同比例的轴。轴 a 和 b 有数字刻度,a 是线性刻度,b 是对数刻度。轴 c 和 d 具有分类标度,c 是序数,因此遵循升序,而 d 只是没有任何特定顺序的类别序列。
图 1-2。
Four axes with different scales
沿着每个轴,最好显示一个标签,简要描述所表示的尺寸;这些被称为轴标签。如果刻度是数字,标签应该在括号中显示测量单位。例如,如果你有一个 x 轴来报告一组数据的时间,你可以把“时间”写成一个轴标签,第二个单位(在这种情况下是秒)用方括号表示为 s )。
图 1-3。
An axis label
在显示图表的绘图区域中,可以包括线网格以帮助数据的可视化对齐。图 1-4 显示了一个图表网格,x 轴为线性时间刻度,y 轴为对数刻度。
图 1-4。
A chart with two different scales
您已经看到了数据是如何用符号表示的。但是,文本标签也可以用来突出显示特定的数据点。点标签在图表中的相应点处提供图表中的值,而工具提示是当鼠标经过给定点时动态显示的小框架。这两种标签如图 1-5 所示。
图 1-5。
The point label and the tooltip of a data point
数据通常被分组到几个系列中,为了在同一个图表中表示这些数据,它们必须是可区分的。最常见的方法是为每个系列分配不同的颜色。在其他情况下,例如,对于折线图,线条(虚线、点线等)也可用于区分不同的系列。一旦建立了颜色(或笔画)序列,就有必要添加一个表格来演示颜色和组之间的对应关系。该表被称为图例,如图 1-6 所示。
图 1-6。
A legend
虽然讨论这一部分中的概念可能显得琐碎,但是定义我将在整本书中引用的元素的术语是很重要的。它们构成了构建图表的基础。您还将看到专门研究图表表示的 JavaScript 库如何使用这些术语,将它们与编辑和设置组件相关联(参见第八章中的“插入选项”一节)。
最常见的图表
本节包含最常见图表类型的简要概述。这些图表将在本书的后续章节中详细描述。
图 1-10。
A candlestick chart
- 蜡烛图:一种专门用于描述价格趋势的图表。每个数据点由四个值组成,通常称为开盘-盘高-盘低-收盘(OHLC)值,并呈现类似烛台的形状(见图 1-10 )。
图 1-9。
A bubble chart and a radar chart
- 气泡图:一个二维散点图,其中第三个变量由数据点的大小表示(见图 1-9 )。
- 雷达图:一种图表,其中一系列数据表示在许多轴上,从图表中心的原点开始呈放射状。这张图表通常呈现蜘蛛网的外观(见图 1-9 )。
图 1-8。
A line chart and a pie chart
- 折线图:由线条连接的有序数据点序列。数据点 P(x,y)在图表中报告,代表 x 和 y 两个轴的刻度(见图 1-8 )。
- 饼图:一个被分成若干段(片)的圆(饼)。每个切片代表一组数据,其大小与百分比值成正比(见图 1-8 )。
图 1-7。
A histogram and a bar chart
- 直方图:竖立在 x 轴上的相邻矩形,分成离散的间隔(区间),其面积与该区间的观察频率成比例(见图 1-7 )。
- 条形图:形状类似于直方图,但本质不同,这是一种矩形条的长度与它们所代表的值成比例的图表。每个条形标识一组数据(见图 1-7 )。
Note
开盘-盘高-盘低-收盘(OHLC)是四个数值,通常用于说明金融工具价格随时间的变化。
如何在 Web 上实现图表
现在,我已经描述了最常见的图表类型和组成它们的元素,下一步是快速浏览一下今天可用的技术,这些技术将允许您实现您的图表。
如今,web 技术处于不断的变化之中:每天都有新的解决方案被提出,解决不久前还被证明非常复杂的问题。这些创新将为您提供实现具有引人注目的图形的高度交互式图表的可能性,所有这些都只需编写几行 JavaScript 代码。整个事情可以快速而容易地完成,因为大部分工作已经由 JavaScript 库为您完成,这些库在图表表示方面非常专业。这些库现在遍布网络。
在本书中,您将使用 jqPlot、Highcharts 和 D3,它们是目前使用最广泛的库,可以为图表实现过程中可能出现的几乎任何问题提供通用解决方案。
但是,在逐一浏览这些库之前(您将在后面的章节中完成),您必须首先调查构成 JavaScript 图表开发基础的所有技术,因为这些技术将伴随您阅读本书的其余部分。
HTML5
最近,有很多关于 HTML5 的讨论,它实际上彻底改变了 web 应用的开发方式。在它出现之前,如果你想引入交互式图形内容,使用 Adobe Flash 等应用几乎是必经之路。但是,使用 Flash 或类似的应用在网上开发图表或其他图形表示有一个明显的限制:依赖于安装在最终用户机器上的插件。此外,智能手机不支持这类应用。由于 HTML5,开发人员现在可以创建高级图形和动画,而不依赖于 Flash。
当你通读这本书时,你会看到 HTML5 是如何导致许多其他技术的诞生的,有些是新的,有些是旧的,但又是更新的,比如 JavaScript。事实上,作为一种语言,JavaScript 正在经历重生,这是由于新库的开发利用了 HTML5 引入的创新。HTML5 有许多新的语法特性,包括
在 Flash 的位置上,您将使用 JavaScript 库,如 jQuery、jqPlot、Highcharts 和 D3。目前,这些是可用于实现任务(如数据的图形可视化)的最广泛和最完整的库。然而,网络技术的世界在不断发展;在互联网上,你总是可以找到新的图书馆,其特征与本书所包含的特征相似。
用 SVG 和画布制作图表
在 HTML5 引入的新技术可以实现的所有可能的图形应用中,我将重点介绍通过图表表示和可视化数据。使用 JavaScript 作为编程语言,我们现在可以利用新浏览器中嵌入的强大渲染引擎。作为这种语言新功能的基础,我将参考 HTML5 canvas 和 SVG。SVG 和 canvas 不是在服务器上绘制静态图像,然后将它们下载到浏览器中,而是允许您开发完全交互式的图表,从而通过内置的动画、过渡和工具提示来丰富您的图形表示。这是因为 SVG 和 canvas 内容是在浏览器中绘制的,所以组成图表的图形元素可以在不刷新页面的情况下进行转换。该功能对于可视化实时数据至关重要,因为实时数据要求图表随着数据的变化而不断更新。以这种方式操作将确保真正的客户端图表。事实上,通过利用这些技术,图表实际上是在客户机上绘制的,只需要从服务器传递数据。这个方面提供了相当多的优点,最重要的是消除了从服务器下载大图形文件的需要。
Canvas vs SVG 的应用
HTML5 canvas 和 SVG 都是允许您在浏览器中创建丰富图形的 web 技术,但它们是根本不同的。贯穿本文,您将主要看到两个 JavaScript 框架:jqPlot 和 D3。基于 jQuery 框架的 jqPlot 利用 HTML5
SVG 是一种基于 XML 的矢量图形格式。SVG 内容可以是静态的、动态的、交互式的或动画的,这使得它非常灵活。您还可以使用层叠样式表(CSS)来设置 SVG 元素的样式,并使用 SVG 文档对象模型(DOM)提供的应用编程接口(API)方法向它们添加动态行为。因此,选择这种格式,您可以获得比简单的矢量图形和动画更多的东西:您可以开发高度交互式的 web 应用,包括脚本、高级动画、事件、过滤器和几乎任何您想象得到的东西。
HTML5 canvas 规范是一个通用的 JavaScript API,允许您编写编程绘制操作。Canvas 本身允许您定义一个 canvas 上下文对象,在 HTML 页面上显示为一个
与 SVG 相比,canvas 的优点是绘图性能高,图形和图像编辑速度更快。每当需要在像素级别工作时,canvas 是更好的选择。然而,对于 canvas,没有 DOM 节点是一个缺点,尤其是如果您不使用 JavaScript 框架,比如 jqPlot。另一个缺点是文本呈现能力差。
与 canvas 相比,SVG 的优势在于分辨率独立性、对动画的良好支持,以及使用声明性语法来激活元素的能力。不过,最重要的是,使用 JavaScript 中的 SVG DOM API 完全控制每个元素。然而,当复杂性增加时,缓慢的渲染可能是一个问题,但浏览器提供商正在努力使浏览器更快(见表 1-1 和 1-2 )。
表 1-1。
Web Browsers and Engines
| 浏览器 | 目前的 | 发动机 | 开发者 | 许可证 | | --- | --- | --- | --- | --- | | 谷歌 Chrome | Twenty-nine | 眨眼 | 谷歌,Opera,三星,英特尔,其他 | GNU 宽松通用公共许可证(LGPL),Berkeley 软件分发(BSD)风格 | | Mozilla Firefox | Twenty-three | 壁虎 | 网景/Mozilla 基金会 | Mozilla 公共许可证(MPL) | | 微软公司出品的 web 浏览器 | Ten | 三叉戟 | 微软 | 所有人 | | 苹果浏览器 | six | 网络工具包 | 苹果,KDE,诺基亚,黑莓,Palm,其他 | GNU lgpl BSD 样式 |表 1-2。
Web Technology Support: Comparison of Web Browsers
| | 浏览器 | | --- | --- | | 技术 | Internet Explorer 10 | 铬 29 | 火狐 23 | 野生动物园 6 | | --- | --- | --- | --- | --- | | SVG(五)。1.1) | | | | | | 过滤 | 是(从 10 开始) | 是 | 是 | 是(从 6 开始) | | 同步多媒体集成语言(SMIL)动画 | 不 | 是 | 是 | 部分的 | | 字体 | 不 | 是 | 不 | 是 | | 片段标识符 | 是 | 不 | 是 | 不 | | HTML 效果 | 部分的 | 部分的 | 是 | 部分的 | | css 背景 | 是 | 是 | 部分的 | 是 | | 半铸钢ˌ钢性铸铁(Cast Semi-Steel) | 是 | 是 | 是 | 是 | | HTML5 | | | | | | 帆布 | 是(从 9 点开始) | 是 | 是 | 是 | | 新元素 | 是 | 是 | 是 | 是 | | 视频元素 | 是(从 9 点开始) | 是 | 是 | 是 | | 百度地图 | | | | | | JavaScript 对象符号(JSON)解析 | 是 | 是 | 是 | 是 | | web GL(web GL) | 不 | 是 | 部分的 | 部分的 |狗
使用在 HTML 页面结构元素层次上工作的库,我们不能避免谈论 DOM。我会经常提到这个概念,因为它是每个网页的基本结构。万维网联盟(W3C)认为有必要为结构化文档的表示创建一个官方标准,以便为所有编程语言和平台开发指导原则,这是正确的。HTML 文档的树结构,以及 XML 文档的树结构,完全遵循了这个标准开发的指导原则。以下是一个 HTML 文档的示例:
<HTML>
<HEAD>
<TITLE>A title</TITLE>
</HEAD>
<BODY>
Hello
<BR>
</BODY>
</HTML>
该文档的 DOM 树可以表示为如图 1-11 所示。
图 1-11。
An example of tree structure of the DOM
但是,DOM 标准并不局限于开发如何在文档中构造 DOM 的指南;该标准还定义了许多功能,用于处理组成文档的元素。因此,任何与文档相关的操作(创建、插入、删除)都应该遵循 DOM 标准。因此,不管您使用的是哪种编程语言,也不管您使用的是哪种平台,您都会发现这个标准表达了相同的功能。通常,术语 DOM 也适用于 API,它管理网页的所有元素。
所有这些都很重要,因为任何选择阅读这本书的人都对开发不仅使用 DOM,而且也是 DOM 的一部分的图表感兴趣,这些图表的每个方面都可以用 JavaScript 检查和操作。在整本书中,您将学习如何最好地利用 jQuery、jqPlot 和 Highcharts (jQuery 扩展)以及 D3 库。使用这些 JavaScript 库,您可以访问每个图表元素,比如更改对象的颜色和位置。
用 JavaScript 开发
尽管大多数选择阅读本书的人可能已经对 JavaScript 有了很好的了解,但事实上可能并非如此。出于这个原因,我以实用的方式组织了这本书,给出了一步一步的例子,并提供了例子中必须编写的所有代码。因此,这本书为新来者提供了一个学习这门语言的机会,也为那些已经有一段时间没有使用这门语言的人提供了一个刷新记忆的机会。
要开始使用将用于开发图表的 JavaScript 库,有必要准备一个开发环境。的确,要开发 JavaScript 代码,您可以简单地使用文本编辑器,比如 Notepad(或者更好的 Notepad++ ),但是开发人员通常更喜欢使用专门的应用(通常称为集成开发环境(ide ))来开发代码。除了为文本编辑器提供与代码中使用的关键字相对应的不同颜色之外,此类应用还包含一组旨在简化工作的工具。这些应用可以检查代码中是否有任何错误,提供调试工具,使管理文件变得容易,并帮助在服务器上部署,以及许多其他操作。
现在网络上有很多 JavaScript IDEs,但最突出的还是 Aptana Studio(见图 1-12);Eclipse Web Developer,安装了 JavaScript 测试驱动程序(JSTD)插件;和 NetBeans。这些编辑器还允许您开发超文本预处理器(PHP)、CSS 和 HTML(有关如何使用 Aptana Studio IDE 建立工作区以实现本书代码的信息,请参见附录 A,或者直接使用本书附带的源代码;您可以在 Apress 网站[ www.apress.com/9781430262893
]的源代码/下载区找到代码示例。
图 1-12。
The Aptana Studio 3 IDE
对于那些不喜欢在计算机上安装太多应用的人,有在线 JavaScript IDEs。这些允许用户在作为 IDE 工作的网页中编辑 JavaScript 代码,并直接从同一个网页中检查结果。不幸的是,许多 ide 都要收费。然而,jsFiddle ( http://jsfiddle.net
)是一个不需要付费的在线 IDE,除了编辑之外,它还提供代码共享和添加库的选项,比如 jQuery 和 D3。(参见图 1-13 )。
图 1-13。
The online IDE jsFiddle
jsFiddle 可以证明非常有用。除了让用户包含许多 JavaScript 库(见图 1-14 )之外,它还提供各自不同的发布版本,从而允许用户实时测试任何不兼容性。
图 1-14。
jsFiddle offers a set of the most popular JavaScript libraries
运行和调试 JavaScript
如果我们想在客户机-服务器框架中定义 JavaScript,它是一种完全的客户端编程语言。它不需要编译,除了 HTML 文档之外,部分代码可以在其他语言特有的许多其他类型的文件中找到,例如。JSP 或. PHP。
这些代码片段不受影响地通过应用服务器,从未被处理过。只有浏览器负责运行 JavaScript 代码。因此,JavaScript 代码仅在下载网页时或之后运行,以响应事件。如果 JavaScript 代码相当大或者以后可能有用,可以在. JS 文件中外部定义它;在这里,您可以找到本文中提到的所有 JavaScript 库和框架。然而,不管其形式如何,JavaScript 都是直接从浏览器运行的。
因此,即使您没有使用真正的 IDE 来开发和调试 JavaScript 代码,您也可以简单地将代码插入到一个空的 HTML 文件中,然后直接在浏览器中加载该文件(Chrome、Internet Explorer 和 Firefox 是最常见的)。为了将它与页面上的其他文本区分开来,您必须将代码放在
<script type="text/javascript">
// JavaScript code
</script>
如果 JavaScript 代码驻留在一个外部文件中,那么有必要将它包含在 HTML 页面中,编写
<script type="text/javascript" src="library.js"></script>
因此,只要 JavaScript 的执行不是出于安装某个东西的目的,您就拥有了所需的一切。谁的操作系统上没有 web 浏览器?
JavaScript 中的数据类型
如前所述,本书既不会解释优秀 JavaScript 代码编程的规则和语法,也不会在编程细节上停留太久。然而,我们将要开发的代码是以图表为中心的,或者说是数据的处理以及如何显示它们。让我们从最简单的例子开始。所有数据结构的最小构造块是变量(当它包含单个值时)。在处理数据类型方面,JavaScript 与其他编程语言有很大不同。,当您希望将 JavaScript 存储在变量中时,您不必指定值的类型(int、string、float、boolean 等);你只需要用关键字var
来定义它。
在 Java 或 C 中,包含整数值的变量与包含文本的变量的声明不同:
int value = 3;
String text = "This is a string value";
在 JavaScript 中,存储值的类型无关紧要。所有东西都用var
声明,所以相同的声明是
var value = 3;
var text = "This is a string value";
因此,在 JavaScript 中,我们可以将变量视为存储任何类型值的容器。
为了简单起见,这里的变量被视为单个值的容器,因此代表了最简单的数据结构。然而,实际上,变量也可能包含更复杂的数据类型:数组和对象。
Note
JavaScript 中变量的使用实际上要复杂一些。你也可以不用关键字var
来声明变量。var
关键字将在当前范围内声明变量。如果缺少var
, JavaScript 将搜索在更高级别的作用域中声明的同名变量。如果 JavaScript 没有找到这个变量,则声明一个新变量;否则,JavaScript 将使用值in the variable found. As a result, an incorrect use of variables can sometimes lead to errors that are difficult to detect.
数组
数组是由逗号分隔并括在方括号[ ]中的一系列值:
var array = [ 1, 6, 3, 8, 2, 4 ];
数组是 JavaScript 中最简单也是最广泛使用的数据结构,所以您应该对它们非常熟悉。通过在括号中指定它的索引(在数组中的位置),可以访问数组中的任何值,紧跟着数组的名称。在 JavaScript 中,索引从 0:
array[3] //returns 8
数组可以包含任何类型的数据,而不仅仅是整数:
var fruits = [ "banana", "apple", "peach" ];
有许多函数可以帮助我们处理这类对象。由于它的有用性,我将在整本书中频繁使用这个对象,因此快速浏览一下它似乎是合适的。
通过书写可以知道数组中值的数量
fruits.length //returns 3
或者,如果您知道这些值,您可以使用
fruits.indexOf("apple") //returns 1
此外,有一组函数允许我们在数组中添加和删除项目。push()和 pop()函数添加和移除数组中的最后一个元素,而 shift()和 unshift()函数添加和移除数组中的第一个元素:
fruits.push("strawberry");
// Now the array is [ "banana", "apple", "peach", "strawberry" ];
fruits.pop(); //returns "strawberry"
// Now the array is [ "banana", "apple", "peach"];
fruits.unshift("orange", "pear");
// Now the array is ["orange", "pear", "banana", "apple", "peach"];
fruits.shift(); //returns "orange"
// Now the array is ["pear", "banana", "apple", "peach"];
有时,有必要对数组中的每个值进行循环,以便对其执行某些操作。在其他编程语言中广泛使用的一种方法是使用函数for()
。例如,要计算数组中值的总和,您应该编写
var sum = 0;
for (var i = 0; i < array.length; i++) {
sum += array[i];
}
但是,在 JavaScript 中更常见的是使用forEach()
函数,其中 d 按照以下顺序一个接一个地假定数组中的值:
var sum = 0;
array.forEach(function(d) {
sum += d;
});
目标
数组对于简单的值列表很有用,但是如果你想要结构化的数据,你需要定义一个对象。对象是一种自定义数据结构,其属性和值由您定义。您可以通过将对象的属性括在两个大括号{ }中来定义对象;每个属性都由一个名称定义,后跟一个冒号(:)和分配的值,每个属性/值对之间用逗号分隔:
var animal = {
species: "lion",
class: "mammalia",
order: "carnivora",
extinct: false,
number: 123456
};
在 JavaScript 代码中,点符号表示每个值,指定属性的名称:
animal.species //Returns "lion"
现在,您已经了解了对象和数组,您可以看到如何将它们结合起来,以便在 JavaScript 中获得更复杂的数据结构。您可以创建对象的数组或数组的对象,甚至对象的对象。方括号用来表示一个数组,花括号,一个对象。例如,让我们以这种方式定义一个对象数组:
var animals = [
{
species: "lion",
class: "mammalia",
order: "carnivora",
extinct: false,
number: 123456
},
{
species: "gorilla",
class: "mammalia",
order: "primates",
extinct: false,
number: 555234
},
{
species: "octopus",
class: "cephalopoda",
order: "octopoda",
extinct: false,
number: 333421
}
];
要获得这些数据结构的值,您需要将方括号与属性的索引和名称一起使用:
animals[0].extinct //return false
animals[2].species //return "octopus"
Firebug 和 DevTools
为了进行调试,如果您使用的是 IDE,可以很容易地利用它附带的各种调试工具。但是,如果您无法访问 IDE,您仍然可以利用外部工具。可以把浏览器想象成一个开发环境,在这里调试工具可以通过可从互联网下载的插件来集成。目前互联网上有很多可用的工具,但我想推荐的是 Firebug,这是一款针对那些喜欢使用浏览器 Mozilla Firefox 的 web 开发工具。Firebug 是一个无缝集成到 Firefox 浏览器中的插件,如图 1-15 所示。
图 1-15。
Firebug is an extention of Mozilla Firefox and is fully integrated into the browser
Firebug 将被证明是一个非常有用的工具,尤其是在使用 jQuery 和 D3 库时,这些库要求 DOM 的结构始终处于控制之下。这个工具将允许你直接监控 DOM 的结构。
然而,对于那些更喜欢使用谷歌 Chrome 的人来说,还有已经集成到浏览器中的 DevTools(见图 1-16 )。要访问该工具,只需单击浏览器右上角的按钮。
接下来,选择工具➤开发人员工具,或者只需右键单击任何页面元素,然后在上下文菜单中选择检查元素。
图 1-16。
With DevTools it is possible to monitor a lot of information about your web page
有了这两个工具,您不仅可以轻松地检查 DOM 的每个元素——它的属性和值——还可以检查应用于它们的 CSS 样式。您还可以输入对这些值的更改来实时观察效果,而不必每次都修改文件中的代码并保存它。Firebug 和 DevTools 还包括各种工具,用于监控页面的性能,包括渲染和联网。
对于 DevTools,还应该特别注意控制台的使用。通过它,您可以使用诸如console.log()
之类的方法来访问诊断信息。此方法经常用于通过控制台显示许多变量的值,将变量的名称作为参数传递,并添加文本作为指示:
var x = 3;
console.log("The value of x is " + x); // The value of x is 3
还可以使用诸如$()
或profile()
的方法输入命令并与文档进行交互。有关这些方法的更多信息,请参见关于控制台 API ( https://developers.google.com/chrome-developer-tools/docs/console-api
)和命令行 API ( https://developers.google.com/chrome-developer-tools/docs/commandline-api
)的文档。
数据
JSON 是一种将数据组织成 JavaScript 对象的特定语法。这种格式通常用于基于浏览器的代码,尤其是 JavaScript。JSON 代表了组织数据的 XML 的有效替代方案。两者都独立于编程语言,但是 JSON 比 XML 更快,更容易用 JavaScript 解析,XML 是一种全标记语言。而且 jqPlot 和 D3 和 JSON 配合的很好。它的结构完全遵循 JavaScript 中定义的对象和数组的规则:
var company = {
"name": "Elusive Dinamics",
"location": "France",
"sites": 2,
"employees": 234,
"updated": true
};
摘要
这第一章已经向你介绍了许多基本概念,它们将伴随你阅读整本书。首先,您研究了最常见的图表类型以及组成它们的元素。您还快速浏览了在着手开发 Web 图表时需要了解的许多技术方面。最后,您简要探讨了本书中 JavaScript 示例中使用的数据类型。
我提到过你的大部分工作将由专门的 JavaScript 库来完成。在下一章,你将学习 jQuery 库。这个库将为您提供一整套直接作用于 DOM 级别的工具。在本书的后面,你会发现关于这个库的知识是至关重要的:许多图形库(包括 jqPlot 和 Highcharts)都是基于它构建的。
二、jQuery 基础知识
Abstract
在前一章中,您了解了 DOM 树,看到了 HTML 文档是如何由许多可以在初始上下文中创建、修改和删除的元素组成的。这些操作由浏览器通过 JavaScript 命令来执行,如前所述,这些命令可以在页面加载时执行,也可以作为后续事件的结果来执行。为此,开发了一个 JavaScript 库,它以一种简单且成熟的方式管理所有这些操作。这个库就是 jQuery,而且是完全开源的。它由 Jon Resig 于 2006 年创建,并由一个开发团队继续改进。与经典 JavaScript 相比,jQuery 非常有用,而且能够操作 DOM 元素,因此它是目前使用最广泛的 JavaScript 库,是所有 web 开发人员的参考点。
在前一章中,您了解了 DOM 树,看到了 HTML 文档是如何由许多可以在初始上下文中创建、修改和删除的元素组成的。这些操作由浏览器通过 JavaScript 命令来执行,如前所述,这些命令可以在页面加载时执行,也可以作为后续事件的结果来执行。为此,开发了一个 JavaScript 库,它以一种简单且成熟的方式管理所有这些操作。这个库就是 jQuery,而且是完全开源的。它由 Jon Resig 于 2006 年创建,并由一个开发团队继续改进。与经典 JavaScript 相比,jQuery 非常有用,而且能够操作 DOM 元素,因此它是目前使用最广泛的 JavaScript 库,是所有 web 开发人员的参考点。
任何计划在网页中包含 jQuery 库的开发人员都会很快发现伴随这个 UI 库的著名格言:“写得少,做得多。”本着这一口号的精神,jQuery 在 JavaScript 代码开发中引入了三个新概念——在使用 thisUI 库提供的方法时,您需要记住这些概念:
- 通过级联样式表(CSS)选择器选择要应用 jQuery 方法的 HTML 页面元素(选择)
- 构建 jQuery 方法链,在同一选择中按顺序应用
- 使用 jQuery 包装器进行隐式迭代
在本章中,在了解了如何在将要开发的网页中包含 jQuery 库之后,将介绍“选择”的概念。选择是 jQuery 库的基础,理解它们以及如何实现它们非常重要,因为它们将在整本书中讨论。通过一系列小示例并使用链接方法的技术,您将浏览 jQuery 库提供的一系列函数,这些函数允许您操作选择,以便创建、隐藏和更改各种 DOM 元素及其内容。在本章的最后一部分,我将介绍 jQuery 用户界面库(jQuery UI ),并举例说明一些最常见的小部件。您将了解它们的基本功能,并发现如何将它们整合到网页中。
本章的目的是提供一个 jQuery 的快速视图——它的功能和基本概念。对每种方法的详细描述超出了本书的范围。在本文的所有例子中,这些方法将根据上下文来解释。但是,您可能还想参考 jQuery 官方网站上的文档( http://jquery.com/
)。
包括 jQuery 库
现在,有两种方法可以将 jQuery 库包含在您的 web 页面中。
- 本地方法:在本地下载必要的库,然后添加到网页中。
- CDN 方法:直接设置链接到提供这些 JavaScript 库的站点。
提供这些库的网站被称为内容交付网络(cdn)。CDN 是一个大型服务器系统,为最终用户提供高可用性和高性能的内容。当用户试图访问文件时,CDN 会选择离用户最近的服务器。最常用的 cdn 之一是谷歌托管图书馆。该服务为 web 应用提供了对许多最流行的开源 JavaScript 库(如 jQuery)的可靠访问。
要从此服务加载库,您需要复制以下 HTML 代码片段,并将其直接粘贴到您的网页中:
<script src="
http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
</script>
另一个可以获得任何版本 jQuery 库的 CDN 站点是 jQuery 本身的官方 CDN 站点: code.jquery.com
。如果您更喜欢使用该网站的 URL,您需要复制并粘贴以下 HTML 片段:
<script src="
http://code.jquery.com/jquery-1.9.1.min.js
Note
本章中的所有例子都使用 1.9.1 版的 jQuery 库。
如果选择本地选项,则需要复制并粘贴 jQuery 库的相对路径。该路径会有所不同,具体取决于库在 web 服务器或 PC 上的位置。创建一个适当的本地目录来加载您需要包含的所有库是一个很好的做法。
例如,您可能决定将您的 jQuery 库放在一个src
目录中,而将您正在开发的 web 页面放在一个charts
目录中,如图 2-1 所示。在这种情况下,您必须使用这个相对路径:
<script src="../src/js/jquery-1.9.1.js"></script>
图 2-1。
An example of how a directory might be organized on a web server Note
有关如何在 web 服务器或 PC 上设置工作区以开发本书中的示例的详细信息,请参见附录 a。您还可以找到关于不同版本的库、如何下载它们以及如何将它们包含在工作区中的信息。
选择
选择是为了以某种方式操作而选择的一组 HTML 元素。实际上,这是 jQuery 背后的主要概念。让我们以清单 2-1 中的简单 HTML 页面为例:
清单 2-1。ch2_01a.html
<HTML>
<HEAD>
<script src="
http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
</HEAD>
<BODY>
<div> This is the first text</div>
<div class="selected"> This is the second text</div>
<div> This is the last text</div>
</BODY>
</HTML>
在此页面中,有三个元素包含三种不同的文本。列表中的第二个元素已经用类名‘selected’
标记。要选择所有三个元素,您可以使用选择器‘div’
,它可以在页面上的所有元素中识别它们。
接下来,编写jQuery()
函数,将选择器作为参数传递。这样,您就已经选择了三个元素及其内容。要获取文本,您可以使用函数text()
,将它链接到jQuery()
函数调用,并在该部分的末尾添加这一行,如清单 2-2 所示。
清单 2-2。ch2_01a.html
<script>
var text = jQuery('div').text();
console.log(text);
</script>
这三个元素中包含的所有文本都被赋给了变量 text。要查看其内容(在调试中非常有用),可以使用函数console.log()
,然后在 Google Chrome 上,直接右键点击页面选择 Inspect element(见图 2-2 )。
图 2-2。
变量 text 包含三个< div >元素中的文本
根据您选择的频率,您也可以用$()
调用这个函数,如清单 2-3 所示。我们将在本书提供的例子中使用这个选项。
清单 2-3。ch2_01b.html
<script>
var text = $('div').text();
console.log(text);
</script>
相反,如果你只想选择三个元素中的一个,你可以通过给每个元素分配一个类名来区分它们,然后将选择器应用于所选元素的名称,而不是标签元素(清单 2-4)。
清单 2-4。ch2_01c.html
<script>
var text = $('.selected').text();
console.log(text);
</script>
在这种情况下,变量 text 只包含第二个元素的文本,如图 2-3 所示。
图 2-3。
Google Chrome 浏览器中 Inspect 元素显示的文本变量的内容
一旦您理解了如何进行选择,您将会发现如何通过改变其内容或属性来操作任何元素。您甚至可以添加其他元素或从页面中删除一个元素。在这方面,由于 jQuery 提供了大量的方法,它为我们提供了必要的工具。
链接方法
jQuery 被设计成允许 jQuery 方法被链接。一旦选择了一个元素或一组元素,下一步就是对它应用一系列方法。这个序列可以用链接来写。
使用前面的例子(见清单 2-1),假设您想用另一个短语替换第二个元素中的文本,并隐藏另外两个元素,使它们不再出现在网页中。为此,您将把“这是第二个文本”替换为一个新行,“这是一个新文本”,同时隐藏其他文本。图 2-4 显示了应用任何更改之前出现的内容。
图 2-4。
不使用 jQuery 方法链的网页显示的文本
现在,您应用以下方法链:
$('div').hide().filter('.selected').text('This is a new text').show();
这三个元素都包含在选择中,然后被隐藏。在您选择的选项中,只有类名为'selected
'的元素及其内容会被替换为新文本。只有最后这些元素必须显示。因此,在这个命令链的末尾,结果将是
包装套件
当涉及到 jQuery 时,我们处理包装器集。在前面的例子中,有三个元素。您通常会选择包含几个元素的选项,但您永远不需要指定编程循环。这里,当您应用hide()
方法来隐藏所有三个元素时,您没有使用for
或while
构造(即$(‘div’).each(function() {}
)。因此,包装集可以被定义为一组可以进行任何操作的元素(选择),就像它是单个项目一样。
jQuery 和 DOM
jQuery 是一个主要在文档对象模型(DOM)上工作的库,并且总是引用它的所有特性。与 DOM 一样,jQuery 将每个 web 页面视为一个树形结构,其中每个标签(也称为元素)都是一个节点。这个树的根是文档,文档是包含 DOM 所有其他元素的元素。jQuery 提供了一组简化这种对象操作的方法,允许您为页面添加动态性。
ready()方法
如果您想编写一个操纵 DOM 元素的 JavaScript 代码,那么在对 DOM 进行操作之前,需要加载 DOM。但是,您需要在浏览器完全加载所有资产之前进行操作。为此,jQuery 为您提供了ready()
方法。这是一个自定义事件处理程序,绑定到文档对象的 DOM。ready()
方法只有一个参数:一个包含代码的函数,这个函数应该在 DOM 加载之后、用户可以在浏览器中看到所有资源之前执行。
$(document).ready( function() {
// we write the JavaScript code here.
});
用选择遍历 DOM
您已经看到了如何选择一组 DOM 元素,使用特定的 CSS 选择器作为标识它们的参数传递。然而,jQuery 的潜力并不止于此;从 DOM 中的选择位置开始,可以遍历 DOM 以获得一组新的要操作的选择元素。jQuery 为我们提供了一组应用于选择的方法。
让我们以清单 2-5 中的简单 HTML 代码为例:
清单 2-5。ch2_03a.html
<HTML>
<HEAD>
<script src="
http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
</HEAD>
<BODY>
<div class="fruits">
<div>Apple</div>
<div>Orange</div>
<div>Banana</div>
<div>Strawberry</div>
</div>
</BODY>
</HTML>
该页面将显示四种水果的列表。正如您已经看到的,如果您使用‘div’
作为选择器进行选择,您将得到五个元素的序列:
<div class="fruits">
<div>Apple</div>
<div>Orange</div>
<div>Banana</div>
<div>Strawberry</div>
</div>
<div>Apple</div>
<div>Orange</div>
<div>Banana</div>
<div>Strawberry</div>
你需要特别注意第一个要素。您将在选择中找到其他四种水果,尽管它们将在连续的元素中重复出现。这是因为选择器‘div’
选择每个元素及其内容,而不管其中的元素是否会被依次选择。当你想对这种类型的选择进行进一步的操作时,考虑到这一点是很重要的。
现在,如果您编写清单 2-6 中的代码片段,您会在一个警告对话框中得到文本,如图 2-5 所示。,您可以看到最后一行中的文本包括所有水果。
清单 2-6。ch2_03a.html
<script>
var text = $('div').text();
alert(text);
</script>
图 2-5。
The alert dialog box shows the text contained within the elements of the selection
但是,通常您需要直接访问选择的特定值。例如,要直接访问当前选择的第二个元素,您可以编写
var text = $('div:eq(1)').text();
您已经将函数eq()
用于您希望选择的元素的索引。现在,你只有这个文本:
同样,如果你想选择序列的第三个元素,你可以直接写
var text = $('div:eq(2)').text();
或者,如果您愿意,您可以进行遍历,使用next()
方法将选择从一个元素移动到下一个元素:
var text = $('div:eq(1)').next().text();
然后,您会收到以下警告消息:
现在,让我们看一个例子,演示选择和 DOM 结构之间的区别。这有时会造成混乱。你必须记住,eq()
方法产生了一种部分选择;next()
、prev()
、parent()
、children()
、nextAll()
和prevAll()
将选择移动到 DOM 上。
事实上,如果你写了这个链
var text = $('div:eq(1)').prev().text();
您不会得到任何东西,因为由‘div:eq(1)’
选择的元素是列表中的第一个(但在选择中是第二个)。因此,如果您试图将选择移动到 DOM 中的前一个元素,您将什么也得不到。如果您想将选择转移到名为‘fruits’
的父元素,您需要使用parent()
方法:
var text = $('div:eq(1)').parent().text();
现在,您获得了父元素,它与选择的第一个元素相同。图 2-6 给出了结果。
图 2-6。
The alert dialog box shows the four fruits within the first element
你写了命令吗
var text = $('div:eq(0)').text();
你会得到同样的结果。
创建和插入新元素
到目前为止,您已经看到,通过在函数jQuery()
或其别名$()
中传递一个参数,您获得了在 DOM 中具有该标签或相同类名的所有项目的选择。现在,假设您将一个不在 HTML 页面中的标签作为参数传递。在这里,您刚刚创建了一个要添加到 DOM 的新项目。此外,该元素实际上是一个选择,因此可能会受到任何类型的操作,即使它尚未实际添加到网页中。通过在方法链的末尾添加一些特定的 jQuery 方法,您将决定在哪里插入新创建的元素。
例如,如前面的示例所示,通过编写代码片段
$('<div>Lemon</div>').appendTo('div:eq(2)');
您在水果列表中创建了一个新元素。然后,将它追加到选择的第三个元素(列表的第二个元素)之后。图 2-7 显示了应用更改后网页中的列表是如何显示的。
图 2-7。
The list can be increased dynamically, adding new elements
有许多方法可以指定在何处以及如何插入刚刚创建的元素:prepend()
、after()
、before()
、append()
、appendTo()
、prependTo()
、insertAfter()
、insertBefore()
、wrap()
、wrapAll()
、wrapInner()
等等。
关于这些函数使用的更多细节,建议读者参考 jQuery 官方网站上的文档( http://jquery.com/
)。
移除、隐藏和替换元素
另一组非常有用的 jQuery 方法包括那些允许我们从页面(从 DOM)中消除静态元素或者至少隐藏它们的方法。有时,这些方法甚至可以用于用一个元素替换另一个元素。
要从列表中删除“橙色”水果,只需简单地写下
$('div:eq(2)').remove();
如果你想隐藏它,你写
$('div:eq(2)').hide();
...
$('div:eq(2)').show();
然而,在这种情况下,在代码的后面,可能会再次显示“Orange”。
如果使用remove()
(见清单 2-7),对应于选择器‘div:eq(2)’
的元素会改变,并且不可能恢复被移除的元素。
清单 2-7。ch2_04c.html
$('div:eq(2)').remove();
var text = $('div:eq(2)').text();
alert(text); //returns 'Banana'
最后,如果您想用“菠萝”替换“橘子”,您可以使用replaceWith()
方法,如下所示:
$('div:eq(2)').replaceWith('<div>Pineapple<div>');
现在,你有了一个新的水果列表,如图 2-8 所示。
图 2-8。
The list can be dynamically reduced by removing some of its elements
jquery ui:widget
除了 jQuery 库,还有另一个库可以帮助您将 web 页面与交互式图形对象集成在一起:jQuery UI。这个库提供了一整套工具,比如小部件、主题、效果和交互,这些工具丰富了 web 页面,将它们变成了高度交互的 web 应用。就我们的目的而言,小部件是我们特别感兴趣的。这些小图形应用可以证明是一个有价值的工具,当添加到您的网页,使您的图表更具互动性。小部件促进了网页下程序的交互,通常是真正的迷你应用。最简单的形式是,小部件以表格、手风琴、组合框甚至按钮的形式出现。
与 jQuery 库一样,如果想要集成插件,您需要在 web 页面中包含插件文件。您还必须包含表示主题的 CSS 文件。这可以通过谷歌托管图书馆服务来实现:
<link rel="stylesheet" href="
http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/themes/smoothness/jquery-ui.css
<script src="
http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
</script>
<script src="
http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js
</script>
您也可以从 CDN jQuery 官方网站下载:
<link rel="stylesheet" href="
http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css
<script src="
http://code.jquery.com/jquery-1.9.1.min.js
<script src="
http://code.jquery.com/ui/1.10.3/jquery-ui.min.js
如果您喜欢在本地下载这些库,或者使用本书附带的源代码中的工作区(参见附录 A),您可以参考下面的库:
<link rel="stylesheet" href="../src/css/smoothness/jquery-ui-1.10.3.custom.min.css" />
<script src="../src/js/jquery-1.9.1.js"></script>
<script src="../src/js/jquery-ui-1.10.3.custom.min.js"></script>
Note
本书中使用的 jQuery UI 小部件的主题是平滑度。可用主题的列表非常庞大,涵盖了许多颜色和形状的组合。这份库存丰富的清单可以在 ThemeRoller ( http://jqueryui.com/themeroller
)上查到。ThemeRoller 是 jQuery 官方网站上的一个页面,允许您预览小部件,然后从可用的小部件中下载您喜欢的主题。
在访问 jQuery UI 的官方网站( http://jqueryui.com/
)时,您会注意到这个库提供了大量的小部件。这里,我将只讨论最常见的例子,尤其是那些最有可能集成到包含图表的页面中的例子。
正如您将在本书中看到的,其中一些小部件将被用作容器,利用它们的特定功能,如调整大小和封装,包括:
- 手风琴
- 制表符
其他小部件将用于取代 HTML 提供的简单控件,因为前者更高级,功能更丰富,包括:
- 小跟班
- 组合框
- 菜单
- 滑块
还有一些小部件也将执行指示器的功能。通过这些,您将看到如何集成一个特定的小部件类:
- 进度条
手风琴
accordion 小部件是一组可折叠的面板,使网页能够在紧凑的空间中显示大量信息。每个面板可以包含一个主题区域,或者,正如您将在后面的章节中看到的,包含不同类型的图表。通过单击每个面板的选项卡来显示内容,允许用户从一个面板移动到另一个面板,而无需更改页面。手风琴的面板根据用户的选择扩展和收缩,使得在任何给定时间只有一个面板显示其内容。
为了在页面中获得一个折叠式小部件,您需要编写的 HTML 结构由一个包含所有面板的外部标签组成。每个面板依次由放置在两个
标签和一对标签之间的标题指定,内容在标签之间。清单 2-8 展示了一个简单的有四个面板的手风琴。
清单 2-8。ch2_05.html
<div id="accordion">
<h3>First header</h3>
<div>First content panel</div>
<h3>Second header</h3>
<div>Second content panel</div>
<h3>Third header</h3>
<div>Third content panel</div>
<h3>Fourth header</h3>
<div>Fourth content panel</div>
</div>
在 JavaScript 代码中,您需要添加清单 2-9 中的代码片段,以便获得一个 accordion 小部件。
清单 2-9。ch2_05.html
$(function() {
$( "#accordion" ).accordion();
});
图 2-9 展示了我们的手风琴。
图 2-9。
An accordion consists of collapsible panels suitable for containing information in a limited amount of space
但是,这还不够。如果能控制手风琴的风格就更好了。这可以通过添加清单 2-10 中给出的代码来实现。
清单 2-10。ch2_05.html
<style type="text/css">
.ui-accordion {
width: 690px;
margin: 2em auto;
}
.ui-accordion-header {
font-size: 15px;
font-weight: bold;
}
.ui-accordion-content {
font-size: 12px;
}
</style>
结果如图 2-10 所示。
图 2-10。
By modifying the CSS style properties, you can change the accordion’s appearance as you like
标签
在功能上与手风琴非常相似的小部件是带有选项卡的面板。这里,每个面板都是唯一的,但是在顶部有几个选项卡,由不同的标题标识。尽管如此,这个小部件提供了在有限的空间内显示大量信息的可能性,用户可以选择一次只查看一个选项卡的内容。更重要的是面板垂直膨胀的损失。
为了在 web 页面中获得一个选项卡小部件,您需要编写的 HTML 结构比前一个稍微复杂一些。标题在一个无序列表
- An anchor tag
清单 2-11。ch2_06.html
<div id="tabs">
<ul>
<li><a href="#tabs-1">First header</a></li>
<li><a href="#tabs-2">Second header</a></li>
<li><a href="#tabs-3">Third header</a></li>
<li><a href="#tabs-4">Fourth header</a></li>
</ul>
<div id="tabs-1">
<p>First tab panel</p>
</div>
<div id="tabs-2">
<p>Second tab panel</p>
</div>
<div id="tabs-3">
<p>Third tab panel</p>
</div>
<div id="tabs-4">
<p>Fourth tab panel</p>
</div>
</div>
在 JavaScript 代码中,您需要指定选项卡小部件,如清单 2-12 所示。
清单 2-12。ch2_06.html
$(function() {
$( "#tabs" ).tabs();
});
CSS 样式类也必须被定义,如清单 2-13 所示。
清单 2-13。ch2_06.html
<style type="text/css">
.ui-tabs {
width: 690px;
margin: 2em auto;
}
.ui-tabs-header {
font-size: 15px;
font-weight: bold;
}
.ui-tabs-panel {
font-size: 12px;
}
</style>
当程序完成时,您将得到如图 2-11 所示的小部件。
图 2-11。
The tab widget consists of multiple panels that occupy the same area
纽扣
在所有可用的小部件中,按钮仍然是最常用的。以前,在网页中插入按钮有两种方法。第一种是经典方法,带有标签<input type="button"/>
。一种更现代的方法是
清单 2-14。ch2_07.html
<button>A button element</button>
<input type="submit" value="A submit button" />
<a href="#">An anchor</a>
在没有进一步说明或 JavaScript 代码的情况下,当您加载页面时,您会看到如图 2-12 所示的按钮。
图 2-12。
The web page shows three types of buttons: a simple button element, a submit button, and an anchor button
要使用 JavaScript 函数引用它们,请编写清单 2-15 中提供的代码片段。
清单 2-15。ch2_07.html
$(function() {
$( "input[type=submit], a, button" )
.button()
.click(function( event ) {
event.preventDefault();
});
});
这样,你将得到一组更美观的按钮,如图 2-13 所示。
图 2-13。
The three types of buttons are now represented by the jQuery UI button widgets
你可以通过添加图标来丰富你的按钮。jQuery UI 提供了大量的图标,但是您也可以使用更大的个人图标。清单 2-16 显示了如何将这四个按钮写入你的网页:
清单 2-16。ch2_08.html
<button>Button with icon only</button>
<button>Button with custom icon on the left</button>
<button>Button with two icons</button>
<button>Button with two icons and no text</button>
您添加了四个按钮来突出显示四种可能的情况:只有一个图标的按钮;左侧带有文本和图标的按钮;每边都有文本和图标的按钮;以及一个有两个图标但没有文本的按钮(见图 2-14 )。查看 HTML 代码,您可以看到实际上所有四个按钮内部都有文本,但是可以禁用此功能以获得没有文本的按钮。清单 2-17 展示了不同按钮的图标分配,图标名称被分配给primary
和secondary
(可选)属性。此外,通过将text
属性设置为‘false’
,可以获得一个没有文本的按钮。
清单 2-17。ch2_08.html
$(function() {
$( "button:first" ).button({
icons: {
primary: "ui-icon-locked"
},
text: false
}).next().button({
icons: {
primary: "ui-icon-italy"
}
}).next().button({
icons: {
primary: "ui-icon-gear",
secondary: "ui-icon-triangle-1-s"
}
}).next().button({
icons: {
primary: "ui-icon-gear",
secondary: "ui-icon-triangle-1-s"
},
text: false
});
});
要插入定制图标,你需要使用函数url(),
将它们的地址定义为一个 CSS 文件,如清单 2-18 所示。
清单 2-18。ch2_08.html
<style>
.ui-button .ui-icon-italy {
background-image: url("icon/exit24x24.png");
width: 24px;
height: 24px;
}
</style>
图 2-14 显示了您刚刚创建的一组按钮。
图 2-14。
Each button can be easily enriched with icons
组合框
组合框是网页和许多应用中另一个广泛使用的控件。组合框是一个可编辑的下拉菜单,用户可以从中选择一个条目。要在你的页面中插入一个组合框,你需要定义一个特定的元素结构,如清单 2-19 所示。
清单 2-19。ch2_09.html
<div class="ui-widget">
<label>Select your destination:</label>
<select id="combobox">
<option value="">Select one...</option>
<option value="Amsterdam">Amsterdam</option>
<option value="London">London</option>
<option value="Rome">Rome</option>
</select>
</div>
接下来,您需要使用 JavaScript 代码引用这个结构,首先用$()
选择元素,然后将该结构激活为一个 jQuery 组合框小部件:
$(function() {
$( "#combobox" ).combobox();
});
让我们添加一些 CSS 样式:
<style>
.ui-widget {
font-size: 18px;
}
</style>
图 2-15 展示了组合框小部件,它代表了一系列事件捕获功能的起点。
图 2-15。
A combo box is a drop-down menu allowing the user to make a choise among various options
菜单
刚刚考虑了组合框,你不能忽视在你的主页上包含一个交互式菜单的可能性。使用这样的菜单,用户可以进行一系列选择,例如选择如何表示图表的选项。
在 HTML 中,无序列表被定义为
- . If you want to add a submenu as a menu item, you only need to insert an embedded unordered list.
清单 2-20。ch2_10.html
<ul id="menu">
<li class="ui-state-disabled"><a href="#">Advanced</a></li>
<li><a href="#">Filter</a></li>
<li>
<a href="#">Zoom</a>
<ul>
<li><a href="#">10%</a></li>
<li><a href="#">25%</a></li>
<li><a href="#">50%</a></li>
<li><a href="#">100%</a></li>
</ul>
</li>
</ul>
与前面的小部件一样,您必须通过添加以下函数来激活菜单:
$(function() {
$( "#menu" ).menu();
});
您还需要包括 CSS 样式设置:
<style>
.ui-menu {
width: 150px;
}
</style>
现在,你在页面上有了一个菜单,如图 2-16 所示。
图 2-16。
A drop-down menu lets you categorize different options
滑块
当你开始开发各种类型的图表时,你会发现每次都要设置几个参数。用户可以通过滑块实时修改这些参数。这些滑块使用户能够在一定范围内更改参数。
与许多其他小部件一样,首先添加元素来表示 web 页面中的滑块。
<div id="slider"></div>
然后,像往常一样,用 JavaScript 函数激活小部件,在。例如,要指定滑块手柄的默认位置,可以将value
属性设置为 0 到 100 之间的百分比值。类似地,可以通过将字符串‘horizontal’
或‘vertical’
分配给orientation
属性来设置方向。对于range
属性,您可以指示滑块覆盖的范围(滑块轨迹的阴影区域)是从'min’
值还是从'max
'值开始(参见图 2-19 )。因此,如果您将范围属性设置为']',范围将从最小值扩展到滑块句柄。animation
属性是另一个需要考虑的设置。slider 小部件内置了动画:当用户点击滑块轨迹时,手柄从当前位置移动到被点击的点;这可以慢慢来,也可以很快来。您可以通过将animation
属性设置为“fast
或“slow’. The attributes ‘true’
来选择手柄移动的速度,并且‘false’
指示动画是启用还是禁用(参见清单 2-21)。
清单 2-21。ch2_12.html
$(function() {
$( "#slider" ).slider({
value: 60,
orientation: 'horizontal',
range: 'min',
animate: 'slow'
});
});
一旦你定义了滑块的基本属性,你必须决定它的大小(和句柄的大小)并添加 CSS 样式设置。当您定义滑块的长度和宽度时,您需要考虑您选择的方向,相应地设置height
和width
属性。在这种情况下,我们希望水平表示一个滑块;因此,width
属性将远远大于height
属性(见清单 2-22)。
清单 2-22。ch2_12.html
<style>
.ui-slider {
width: 400px;
height: 10px;
}
.ui-slider .ui-slider-handle {
width: 12px;
height: 20px;
}
</style>
如果在浏览器中加载网页,可以看到滑块(见图 2-17 )。
图 2-17。
A slider is a widget that allows you to select a numeric value in a range
有时,您将需要使用多个滑块;你需要水平地组织它们(你可以找到类似的结构,例如,在立体声的均衡器中)。当指定几个滑块时,没有必要定义多个元素:只需要一个元素,用"eq"
作为它的id
来标记它。然后,在这个元素中,将每个滑块定义为包含其默认值的一对滑块(即各自的句柄出现在滑块轨道上的位置),如清单 2-23 所示。
清单 2-23。ch2_13.html
<div id="eq">
<span>88</span>
<span>77</span>
<span>55</span>
<span>33</span>
<span>40</span>
<span>45</span>
<span>70</span>
</div>
现在,您必须实现一个 JavaScript 函数,这次稍微复杂一些。首先,使用$("#eq > span")
选择器,对七个元素进行选择。然后,使用parseInt()
函数,将所有成对包含的值分配给相应的value
属性,以便句柄位于图 2-18 所示的位置(参见清单 2-24)。
清单 2-24。ch2_13.html
$(function() {
$( '#eq > span' ).each(function() {
// read initial values from markup and remove that
var value = parseInt( $( this ).text(), 10 );
$( this ).empty().slider({
value: value,
range: 'min',
animate: 'slow',
orientation: 'vertical'
});
});
});
即使对于这种类似均衡器的结构,也有必要添加一些 CSS 样式设置,比如不同滑块之间的边距(见清单 2-25)。
清单 2-25。ch2_13.html
<style>
#eq span {
height:180px;
float:left;
margin:15px;
width:10px;
}
</style>
最终,你会得到如图 2-18 所示的条形。
图 2-18。
The sliders can also be grouped in series to achieve more complex controls (e.g., an equalizer)
进度条
当您开发复杂的操作时,系统可能需要很长时间来完成其任务。当用户处于等待状态时,为了防止系统看起来被锁定,通常用进度条来表示进程完成的百分比。定义进度条非常简单:
<div id="progressbar"></div>
您还必须用 JavaScript 编写相应的函数来激活进度条,如清单 2-26 所示。
清单 2-26。ch2_11a.html
$(function() {
$( "#progressbar" ).progressbar({
value: 37
});
});
接下来,添加 CSS 样式设置,如清单 2-27 所示。
清单 2-27。ch2_11a.html
<style>
.ui-progressbar {
height: 20px;
width: 600px;
}
</style>
但是,你得到的并不是你想要的结果;你得到一个静态进度条,固定在 37%的标记处(见图 2-19 )。
图 2-19。
With a progress bar, you can display the status of a process
为了获得一个全功能的进度条,您需要用一个直接连接到流程底层迭代的计数器值来设置它的属性value
。此外,如果想增加进度条的动态效果,可以使用动画图形交换格式(GIF)图像作为背景。清单 2-28 显示了进度条中 CSS 样式属性的添加。
清单 2-28。ch2_11b.html
<style>
.ui-progressbar {
height: 20px;
width: 600px;
}
.ui-progressbar .ui-progressbar-value {
background-image: url(img/pbar-ani.gif);
}
</style>
图 2-20 中的 GIF 图像给人一种更好的操作进度感。
图 2-20。
A progress bar with an animated GIF gives a highly dynamic appearance to the web page Note
从网站 ajaxload ( www.ajaxload.info
)可以轻松安全地获得适合任何种类进度条的动画 gif。只需选择你想使用的进度条类型,然后选择前景和背景颜色,网站就会自动生成动画 GIF 的预览。如果你喜欢这张图片,你可以继续下载。
否则,您可以使用本书附带的代码中的动画 GIF ( pbar-ani.gif
),在 charts/images 目录中(您可以在 Apress 网站[ www.apress.com/9781430262893
]的源代码/下载区找到代码示例)。
关于 jQuery 库的总结性思考
现在,您可能想知道为什么我们从一个库(jQuery)开始,这个库显然与图表的开发或一般的数据可视化没有任何关系。您已经看到 jQuery UI 库为我们提供了图形元素,但是它的用途与您想象中的图表相去甚远。
实际上,我们必须从这里开始。您已经决定使用 JavaScript 语言,目的是在 web 页面中实现图形元素(只不过是 DOM 元素)。所有这些的核心是通过这个库引入的概念。选择、方法链、结构、使用 CSS 样式的实践——这些是 web 编程的基础,更是图表开发的基础。还有什么比使用 jQuery 库更好的方法来获得这些基础知识呢?
随着本书的深入,您会发现您操作的大多数 JavaScript 库(jqPlot、Highcharts 等等)都必须包含 jQuery 库。即使是不使用 jQuery 的 D3 库,其结构也能够管理选择、方法链和结构——也就是说,这些概念现在形成了 JavaScript 开发的基础。
这就是为什么了解 jQuery 很重要。
摘要
在开始直接用 JavaScript 开发图表之前,有必要介绍一些基本工具,它们构成了开发这类代码的基础。在第二章中,我们向您介绍了 jQuery 和 jQuery UI 库。使用 jQuery,您学习了如何通过选择和方法链动态操作 DOM 元素。通过 jQuery UI,您发现了如何用交互式图形元素来丰富页面:jQuery UI 小部件。
在下一章,你将开始运用你目前所学的一切来实现你的图表。您将从处理传入数据开始,这将通过解析一个 HTML 表来完成。
三、简单的 HTML 表格
Abstract
HTML 页面中最简单也是最广泛使用的数据显示形式之一是 HTML 表格。正因为它的广泛用途,表格是 HTML 中最早开发的元素之一。
HTML 页面中最简单也是最广泛使用的数据显示形式之一是 HTML 表格。正因为它的广泛用途,表格是 HTML 中最早开发的元素之一。
在这一章中,你将看到一个表格是如何构造的,以及实现它的 HTML 标签。正确使用这些标记可以区分一个可读的表和一个不可理解的表,后者不允许您理解潜在的关系。
然后,您将构建一个包含数据的表,您将在这里和接下来的章节中使用这些数据。这个例子的目的是理解一个表的本质以及数据在表中是如何组织的。这是构建最适合特定数据结构的图表类型的关键一步。
负责数据可视化的图表完全用 JavaScript 实现。因为您需要在图表中表示的数据包含在 HTML 表中,所以在本章的第二部分,您将看到如何用 JavaScript 语言实现一系列解析器。使用 jQuery 库,您会发现实现读取 HTML 表中特定数据的解析器是多么容易。这些以数组形式收集的数据很容易从 JavaScript 语言中获得,也很容易操作。
为您的数据创建表
表格只是一个嵌套标签的结构,以<table>
标签为根。构建这种结构的过程并不困难,但需要一些深谋远虑。首先,你需要在一张纸上画出桌子的草图,或者至少,对于那些更熟悉桌子的人来说,在心里画出。这有助于确定表格中应包含的列数、行数和标题数。在标签对<table></table>
中,根据需要插入与<tr></tr>
对一样多的行。每个<tr>
标记在表中创建一行。然后,您必须定义单元格。通常,顶行包含标题,因此您必须指定它们。您使用<th></th>
对来指示应该被视为标题的文本。为了指定普通单元格,您使用了一对标签<td></td>
。您必须小心保持行内单元格的数量一致。
还有其他一些标签可以实现丰富表格结构的功能。<caption>
标签通常紧接在开始的<table>
标签之后,呈现时,<caption>
标签中的内容显示在表格上方的中间。标签<thead>
、<tfoot>
和<tbody>
极大地改善了表格结构,并为级联样式表(CSS)和 JavaScript 提供了额外的挂钩。
这是用 HTML 构建表格结构的基本过程。现在,为了更好地理解,您将创建一个带有简单示例的表。
你的例子的目标
不幸儿童共和国统计局最近公布了关于太空中丢失的气球数量的结果。您希望将这个值放在一个 HTML 表中。
通过这个简单的例子,您将熟悉 HTML 表中的数据结构,学习如何应用 jQuery 选择来提取其中包含的数据。
此外,您将发现 CSS 样式在表格图形方面所起的作用。通过改变颜色和文本样式,您可以创建各种各样的图形主题。您还将看到如何通过使用渐变来调整背景颜色,以便为表格中的单元格提供三维外观。
清单 3-1 提供了一组数据,包括几个国家在六个月内每月丢失的气球数量。
清单 3-1。ch3_01.html
<HTML>
<HEAD>
<TITLE>MyChart</TITLE>
</HEAD>
<BODY>
<table class="myTable">
<caption>Balloons Lost in Space</caption>
<thead>
<tr>
<td></td>
<th>May 2013</th>
<th>Jun 2013</th>
<th>Jul 2013</th>
<th>Aug 2013</th>
<th>Sep 2013</th>
<th>Oct 2013</th>
</tr>
</thead>
<tbody>
<tr>
<th>USA</th>
<td>12</td>
<td>40</td>
<td>75</td>
<td>23</td>
<td>42</td>
<td>80</td>
</tr>
<tr>
<th>Canada</th>
<td>3</td>
<td>22</td>
<td>40</td>
<td>27</td>
<td>35</td>
<td>21</td>
</tr>
<tr>
<th>Australia</th>
<td>60</td>
<td>80</td>
<td>16</td>
<td>28</td>
<td>33</td>
<td>26</td>
</tr>
<tr>
<th>Brazil</th>
<td>46</td>
<td>7</td>
<td>14</td>
<td>26</td>
<td>36</td>
<td>24</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="7">Data from Statistical Office of the Republic of Unhappy Children</td>
</tr>
</tfoot>
</table>
</BODY>
</HTML>
这个简单的 HTML 代码给出了如图 3-1 所示的结果。
图 3-1。
A raw HTML table without any CSS style
如你所见,这张桌子完全没有任何图形。它以表格结构中的一系列字符串的形式出现,但仅此而已。这是 CSS 样式发挥作用的时刻。
将 CSS 应用到您的表格
CSS 丰富了表格的图形模式,使其更具可读性,同时也更具吸引力。HTML 页面的每个元素都可以引用 CSS 样式类,并且可以通过设置这些类的属性来调整其图形特性。通过这种方式,您可以根据自己的喜好使用 CSS 来设计表格的样式。可以为 HTML 页面的任何元素设置多个样式属性。这要归功于 CSS3。
Note
这本书没有详细讨论 CSS 样式,也没有列出它们所有的可能性。这是一个庞大的主题,对我们来说,深入讨论可能会产生误导。然而,本书中的具体案例列出了所有需要设置的属性,从而让您更加熟悉 CSS 的广阔世界。
类及其属性的定义以这种方式写在对<style></style>
中:
element.class {
attribute: value;
}
或者,如果您愿意,可以将这些定义编写在一个 CSS 文件中,然后包含在一个或多个网页中。因此,您可以为您的表定义以下 CSS 样式类,在 web 页面的<head>
部分编写代码行,如清单 3-2 所示。
清单 3-2。ch3_02a.html
<style type="text/css">
table.myTable caption {
font-size: 14px;
padding-bottom: 5px;
font-weight: bold;
}
table.myTable {
font-family: verdana, arial, sans-serif;
font-size:11px;
color:#333333;
border-width: 1px;
border-color: #666666;
border-collapse: collapse;
}
table.myTable th {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666666;
background-color: #bbd0da;
}
table.myTable td {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666666;
background-color: #dedede;
}
</style>
现在,如果您再次加载网页,您可以看到表格的新布局,如图 3-2 所示。
图 3-2。
An HTML table in which you applied some CSS styles
查看表格,您可以看到数据现在更容易阅读,外观也更美观。上面的图示只是无数可能组合中的一种。可以设置的属性如此之多,以至于表的形式几乎没有限制。您还可以集成图像和背景,以进一步增强图形功能。
为您的表格添加颜色渐变
现在,您将继续优化您的表的外观。你已经取得了显著的进步,但你还可以更进一步。
正如您所看到的,表格中单元格的背景颜色是统一的,但是您可以创建颜色渐变,为 CSS 属性分配特定的值。因为这可能有些困难,网页终极 CSS 渐变生成器( http://www.colorzilla.com/gradient-editor
)可以作为一个有用的工具,通过允许您选择颜色和它们将采取的方向,帮助您以图形方式生成渐变(见图 3-3 )。
图 3-3。
Ultimate CSS Gradient Generator allows you to generate CSS gradients very easily
从最终的 CSS 渐变网页中,让我们选择两个我们喜欢的预设,为灰色单元格选择预设的灰色 3D #4,为标题单元格选择蓝色管道#2。接下来,复制 CSS 属性并粘贴到你的网页中,如清单 3-3 所示。
清单 3-3。ch3_02b.html
table.myTable th {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666666;
background: rgb(225,255,255); /* Old browsers */
background: -moz-linear-gradient(top, rgba(225,255,255,1) 0%,
rgba(225,255,255,1) 7%,
rgba(225,255,255,1) 12%,
rgba(253,255,255,1) 12%,
rgba(230,248,253,1) 30%,
rgba(200,238,251,1) 54%,
rgba(190,228,248,1) 75%,
rgba(177,216,245,1) 100%); /* FF3.6+ */
background: -webkit-gradient(
linear, left top, left bottom,
color-stop(0%,rgba(225,255,255,1)),
color-stop(7%,rgba(225,255,255,1)),
color-stop(12%,rgba(225,255,255,1)),
color-stop(12%,rgba(253,255,255,1)),
color-stop(30%,rgba(230,248,253,1)),
color-stop(54%,rgba(200,238,251,1)),
color-stop(75%,rgba(190,228,248,1)),
color-stop(100%,rgba(177,216,245,1))); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(
top,
rgba(225,255,255,1) 0%,
rgba(225,255,255,1) 7%,
rgba(225,255,255,1) 12%,
rgba(253,255,255,1) 12%,
rgba(230,248,253,1) 30%,
rgba(200,238,251,1) 54%,
rgba(190,228,248,1) 75%,
rgba(177,216,245,1) 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(
top,
rgba(225,255,255,1) 0%,
rgba(225,255,255,1) 7%,
rgba(225,255,255,1) 12%,
rgba(253,255,255,1) 12%,
rgba(230,248,253,1) 30%,
rgba(200,238,251,1) 54%,
rgba(190,228,248,1) 75%,
rgba(177,216,245,1) 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(
top,
rgba(225,255,255,1) 0%,
rgba(225,255,255,1) 7%,
rgba(225,255,255,1) 12%,
rgba(253,255,255,1) 12%,
rgba(230,248,253,1) 30%,
rgba(200,238,251,1) 54%,
rgba(190,228,248,1) 75%,
rgba(177,216,245,1) 100%); /* IE10+ */
background: linear-gradient(
to bottom,
rgba(225,255,255,1) 0%,
rgba(225,255,255,1) 7%,
rgba(225,255,255,1) 12%,
rgba(253,255,255,1) 12%,
rgba(230,248,253,1) 30%,
rgba(200,238,251,1) 54%,
rgba(190,228,248,1) 75%,
rgba(177,216,245,1) 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient(
startColorstr='#e1ffff', endColorstr='#b1d8f5',GradientType=0 ); /* IE6-9 */}
table.myTable td {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666666;
background: rgb(242,245,246); /* Old browsers */
background: -moz-linear-gradient(
top,
rgba(242,245,246,1) 0%,
rgba(227,234,237,1) 37%,
rgba(200,215,220,1) 100%); /* FF3.6+ */
background: -webkit-gradient(
linear, left top, left bottom,
color-stop(0%,rgba(242,245,246,1)),
color-stop(37%,rgba(227,234,237,1)),
color-stop(100%,rgba(200,215,220,1))); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(
top,
rgba(242,245,246,1) 0%,
rgba(227,234,237,1) 37%,
rgba(200,215,220,1) 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(
top, rgba(242,245,246,1) 0%,
rgba(227,234,237,1) 37%,
rgba(200,215,220,1) 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(
top,
rgba(242,245,246,1) 0%,
rgba(227,234,237,1) 37%,
rgba(200,215,220,1) 100%); /* IE10+ */
background: linear-gradient(
to bottom,
rgba(242,245,246,1) 0%,
rgba(227,234,237,1) 37%,
rgba(200,215,220,1) 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient(
startColorstr='#f2f5f6', endColorstr='#c8d7dc',GradientType=0 ); /* IE6-9 */
您会立即注意到添加的代码相当广泛。关于属性背景上渐变的应用,每个浏览器都有不同的规范。因为网站的用户可以从任何类型的浏览器请求您的页面,所以您必须考虑到所有的可能性。
在图 3-4 中,您可以看到如何将渐变应用于不同的单元格,从而使表格呈现三维外观。
图 3-4。
The CSS background attribute can be set with gradients to give the table a better appearance
使用文件向表格添加颜色渐变
颜色渐变是您可以为表格选择的另一种样式属性。以下示例使用背景图像,使表格的单元格具有颜色渐变。为了实现这一点,您必须包含两个.jpg
文件,其中您绘制了两种颜色等级:蓝色用于标题单元格,灰色用于具有常规值的单元格,如清单 3-4 所示。
清单 3-4。ch3_02c.html
table.myTable th {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666666;
background:#b5cfd2 url('img/cell-blue.jpg');
}
table.myTable td {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666666;
background:#dcddc0 url('img/cell-grey.jpg');
}
Note
用来给表格的单个单元格着色的两个.jpg
背景文件可以在本书附带的代码中找到,在 Apress 网站的源代码/下载区( www.apress.com
)。
图 3-5 说明了同一个 HTML 表格,但是这一次,使用两幅图像作为背景,褪色的颜色模拟阴影,使表格具有三维外观。
图 3-5。
The same HTML table, but with another CSS style
您已经看到了如何构建 HTML 表格以及如何为其分配 CSS 样式。现在,让我们看看 JavaScript 将在整本书中扮演的角色:数据分析和图表显示。在这种情况下,输入数据以表格的形式表示,但也可以是从数据库中获得的数据或从文件中读取的数据。
解析表数据
前一章描述了 jQuery 库的基础知识。借助本库提供的函数,在本章和后面的章节中,您将开发不同种类的图表,在表格中显示数据。但是,您应该将 JavaScript 代码放在哪里呢?在<script></script>
标记对中,您将添加$(document)。ready()函数,在其中编写 JavaScript 代码,以便在窗口加载之前添加所有事件或您想要包含的任何内容。一旦浏览器注册了文档对象模型(DOM ),您在括号中写的所有内容都会被执行。这使您可以在页面打开之前隐藏或显示页面的元素。
导入 jQuery 库
要使用 jQuery 函数,您必须导入 jQuery 库。没有必要从 jQuery 网站下载并保存到您的服务器上;网页可以从分发站点直接访问库。清单 3-5 代表了您将要编写的 JavaScript 代码的起点。
清单 3-5。ch3_03a.html
<script src="
http://code.jquery.com/jquery-1.9.1.min.js
<script>
$(document).ready(function(){
//add your code here
});
</script>
Note
如果您使用的是该书附带的源代码,请将对 jQuery 库的引用替换为
<script type="text/javascript" src="..
/src/jquery.min.js"></script>
一旦准备工作完成,您就可以立即开始编写代码了。首先,您将创建一个名为tableData
的 JavaScript 对象来保存所有信息:
<script>
$(document).ready(function(){
var tableData = {};
});
该变量将用作从表中解析的数据的容器。使用括号是为了将它视为一个对象。通过声明tableData.myNewProperty
并将该属性设置为您选择的任何值,您可以向该对象添加任何属性。同样,您可以将表格元素存储在另一个变量中:
$(document).ready(function(){
var tableData = {};
var table = $('table');
});
使用这个语句,您可以指向包含在<table></table>
标记对中的所有元素。只用几个字,你就做出了选择。
标签
您在tableData
对象中创建的第一个属性是xLabels
。它将包含 x 轴上标签的值。这些标签对应于表格标题的单元格内容。标签按照收集数据的顺序读取,即从左到右。您可以在<th>
标签中找到这些值(参见图 3-6 )。
图 3-6。
Parsing the headings with the 'thead th'
as selector
在清单 3-6 中,xLabels
被定义为一个数组,通过使用each()
方法,每个<th>
元素都被遍历,并将其内容推入这个数组。仔细看看这个表,可以看到<th>
元素嵌套在<thead>
标签中。因此,正确的选择需要您用'thead th'
指定层级。
清单 3-6。ch3_03b.html
$(document).ready(function(){
var tableData = {};
var table = $('table')
tableData.xLabels = [];
table.find('thead th').each(function(){
tableData.xLabels.push( $(this).html() );
});
});
只是为了调试,为了查看这些数组的内容,您可以使用控制台(参见第一章的中的“Firebug 和 DevTools”一节),调用log()
函数来显示作为参数传递的变量的内容:
console.log(tableData.xLabels);
这样你就定义了一个新的数组xLabels
,包含了如图 3-7 所示的值。
图 3-7。
The content of the xLabels
array, displayed in Firebug
提取标签
现在你需要从表格中提取标签,引用一系列数据,如图 3-8 所示。在您的表格中,您可以用左侧显示的国家名称来识别这些标签。
图 3-8。
Parsing the names of countries with 'tbody th'
as selector
为了捕获标签,您认为它们被分组为<tbody>
组中的<th>
元素。与xLabels
一样,您定义了一个新的属性tableData
: legend
(见清单 3-7)。之所以指定这个名称,是因为在任何类型的图表中,系列的标识符通常在图例中报告。因此,您编写了一个与前一个类似的代码,这次使用选择'tbody th'
。
清单 3-7。ch3_03c.html
$(document).ready(function(){
...
table.find('thead th').each(function(){
tableData.xLabels.push( $(this).html() );
});
tableData.legend = [];
table.find('tbody th').each(function(){
tableData.legend.push( $(this).html() );
});
});
使用 Firebug 上的控制台,可以看到legend
数组的内容,如图 3-9 所示。
图 3-9。
The content of the legend
array, displayed in Firebug
通过分析表中列出的点,你可以很容易地区分什么会在 x 轴上,什么会在 y 轴上。您收集了几个系列(国家)在不同时间(月)的数据。很容易看出,时间将在 x 轴上表示,用xLabels
填充其刻度;您为每个分笔成交点分配一个月。此外,整个系列的值必须分布在 y 轴上。您事先不知道 y 轴上刻度的数值,也不知道需要多少个刻度,但是您需要计算它们。首先,在这些情况下,通常的做法是找出数据中的最高值和最低值。您可以用'tbody td'
选择器选择所有数据,如清单 3-8 所示。
清单 3-8。ch3_03d.html
$(document).ready(function(){
...
table.find('tbody th').each(function(){
tableData.legend.push( $(this).html() );
});
var tmp = [];
table.find('tbody td').each(function(){
var thisVal = parseFloat( $(this).text() );
tmp.push(thisVal);
});
if(Math.min.apply(null, tmp) > 0)
tableData.minVal = 0;
else
tableData.minVal = Math.min.apply(null, tmp);
tableData.maxVal = Math.max.apply(null, tmp);
});
您希望 y 轴上的最小值等于 0,并且仅当表中有负值时才采用更低的值。这种情况下只有正数,所以minVal
是 0,maxVal
是 80。
然后,使用这两个值来计算 y 标签上的刻度。基于这些值,您应该在 0 到 80 的范围内延伸 y 轴。其实最好是把最大值延长,不要让你的数据的最大值点碰到图表顶部。您可以将最大值乘以一个系数(例如,10%)。让我们修正清单 3-8 的最后一行,引入系数,如清单 3-9 所示。
您将从Math.max.apply()
函数返回的最大值乘以因子 1.1,从而将该值增加 10%(max
+0.1*max
= 1.1*max
)。
清单 3-9。ch3_03e.html
if(Math.min.apply(null, tmp) > 0)
tableData.minVal = 0;
else
tableData.minVal = Math.min.apply(null, tmp);
tableData.maxVal = 1.1 * Math.max.apply(null, tmp);
关于刻度的数量和它们的内容,清单 3-10 定义了一个yLabels
数组作为tableData
的属性。要量化 y 轴上的刻度数,使其代表最佳折衷,您必须首先确定一个刻度与下一个刻度之间的合适距离(以像素为单位)。将 y 轴的范围除以一个数字,该数字表示刻度之间的像素距离。你可能认为 30 像素的距离就足够了。结果不是整数,需要上舍入。
清单 3-10。ch3_03f.html
$(document).ready(function(){
...
tableData.maxVal = 1.1 * Math.max.apply(null, tmp);
tableData.yLabels = [];
var yDeltaPixels = 30;
var h = 360;
var w = 460;
var nTicks = Math.round(h / yDeltaPixels);
var yRange = tableData.maxVal - tableData.minVal;
var yDelta = Math.ceil(yRange / nTicks);
var yVal = tableData.minVal;
while( yVal < (tableData.maxVal - yDelta)){
tableData.yLabels.push(yVal);
yVal += yDelta;
}
tableData.yLabels.push(yVal);
tableData.yLabels.push(tableData.maxVal);
});
如果您研究yLabels
数组的内容,您会发现 y 的 12 个值对应于 12 个刻度,如图 3-10 所示。这些标签将显示在每个刻度旁边。
图 3-10。
The content of the yLabels
array, displayed in Firebug
yLabels
数组中的值取决于许多因素,例如,您想要表示 y 轴的维度(这里,您选择了 360 个像素),每个刻度间隔 30 个像素:这里您得到的是 12 个刻度。另外,如果您想知道 y 的多少个单位对应于一个刻度和下一个刻度之间的距离,您可以计算yDelta
(向上取整),在本例中是 8。事实上,yLabels
的值都是 8 的倍数。
数据组
您需要创建的下一个属性是dataGroups
,一个包含所有值的两级数组,按系列分组。每个数列是一个数值数组,dataGroups
是一个数列数组。要对不同系列的数据进行分组,可以使用<tr>
标签。事实上,表中的每一行都是一个序列,您可以获取其中的所有值,因为它们是由单元格标记<td>
分隔的。结合标题中报告的时间方向,从左至右读取数据(见图 3-11 )来获取数值。
图 3-11。
Parsing of groups of data for multiseries
接下来,使用'tbody tr'
作为选择器,遍历表中的行(见清单 3-11);对于该迭代的每一步,您都必须对每个单元格进行循环,以便获得值。
清单 3-11。ch3_03g.html
$(document).ready(function(){
...
tableData.yLabels.push(tableData.maxVal);
tableData.dataGroups = [];
table.find('tbody tr').each(function(i){
tableData.dataGroups[i] = [];
$(this).find('td').each(function(){
var tdVal = parseFloat( $(this).text() );
tableData.dataGroups[i].push( tdVal );
});
});
});
最后可以看到dataGroups
数组中包含的四个数组(每个系列一个),如图 3-12 所示。
图 3-12。
The content of the dataGroups
array, displayed in Firebug
如果您想要访问特定系列中包含的值,您可以按如下方式操作:
console.log(tableData.dataGroups[0]);
console.log(tableData.dataGroups[1]);
...
准备实现图形
现在,您已经提取了表中的所有数据并将它们放在单独的数组中,您已经准备好开始实现图形并将这些数据转换成图形元素。
这将是接下来三章的主题,在这三章中,您将看到这些数据首先以折线图(第四章)表示,然后以条形图(第五章)表示,最后以饼图(第六章)表示。
摘要
本章的目的是介绍当你有一个数据结构要操作时,你必须遵循的方法。通过本章,您开始了解如何基于 jQuery 库提供的工具开发自己的库。
您从创建 HTML 表开始,这是最原始的数据表示形式。尽管它很简单,但是如果设置不好,这个表可能会有问题。选择表格形式的数据表示作为试验场,开始研究 jQuery 如何选择页面上的特定 HTML 元素,更具体地说,是组成 HTML 表格的标记。在此基础上,您构建了一组解析器来从复杂的结构中提取数据(在本例中是一个 HTML 表,但是正如您将看到的,也可以从其他类型的结构中提取数据)。这些数据因此被分成不同的组,以一种更容易操作的格式。
在下一章中,您将开始使用画布提供的第一批图形元素,同时继续使用 jQuery 库。作为第一步,您将学习如何使用通过本章开发的解析器获得的数据来开发折线图。
四、绘制折线图
Abstract
在前一章中,您构建了一个 HTML 表作为结构化数据的样本,并用 JavaScript 开发了解析器来将数据提取到数组中。在本章中,您将使用 JavaScript 构建一个折线图,作为第三章表格中数据的一种可能的可视化方式。
在前一章中,您构建了一个 HTML 表作为结构化数据的样本,并用 JavaScript 开发了解析器来将数据提取到数组中。在本章中,您将使用 JavaScript 构建一个折线图,作为第三章中表格数据的一种可能的可视化方式。
在开始将数据转换为图形元素以制作折线图之前,您需要一个使用它的地方。因此,我将从介绍画布开始这一章。您将看到这是什么以及如何实现它,最后,您将把它集成到您的 web 页面中。
一旦你理解了画布是什么,你就可以开始实现构成折线图的元素。您要处理的第一个组件是轴,根据要表示的数据,您将在轴上应用记号和标签,然后是网格。背景。最后,您将通过绘制代表从 HTML 表中读取的数据的线条来完成折线图。
本章的最后一部分解释了如何在折线图中添加其他不太重要但非常重要的组件:标题和图例。
定义画布
HTML5 技术允许你在你的网站上定义一个叫做 canvas 的绘图区域。这个区域被定义为一个真实的标签元素,它根据标签<canvas>
插入的位置在一个确定的位置占据一个定义的区域。
在前一章中,您首先开发了一个包含一些数据的 HTML 表,然后开发了一组解析器,以便可以通过数组访问这些数据。从您离开的地方继续,现在添加<canvas>
元素,然后您可以在其上绘制折线图的所有元素。
因此,让我们在<body>
部分插入<canvas
>元素,就在你想要为你的图表保留一个区域的地方(见清单 4-1)。然后,您必须在与表格相关的所有其他标记之前插入以下标记,以便在加载页面时,您的图表显示在表格上方。
清单 4-1。ch4_01.html
<canvas id="myCanvas" width="500" height="400"> </canvas>
...
在这些标记语句中,您还指定了绘图区域的大小。为了避免绘图与表格重叠,您可以将 HTML 表格移动到画布下方。因此,在层叠样式表(CSS)样式中,您需要定义表格的新位置,如清单 4-2 所示。top
属性定义了从网页上边缘的距离,而left
属性定义了表格从左边缘的距离。
清单 4-2。ch4_01.html
迷失在太空中的气球五、绘制条形图
Abstract
在上一章中,您使用 HTML 表格中的数据创建了一个折线图。但是,相同的数据可以用其他图表表示,包括众所周知的条形图。使用这种类型的数据可视化,您仍然有两个轴(x 和 y ),但是 x 不是表示为连续的数值范围,而是用于表示组,组可能会也可能不会遵循增长趋势。
在上一章中,您使用 HTML 表格中的数据创建了一个折线图。但是,相同的数据可以用其他图表表示,包括众所周知的条形图。使用这种类型的数据可视化,您仍然有两个轴(x 和 y ),但是 x 不是表示为连续的数值范围,而是用于表示组,组可能会也可能不会遵循增长趋势。
在本章中,您将继续使用前一章中编写的代码,并且您将看到如何通过对代码进行一些更改(非常少!),可以将折线图转换成条形图。一旦实现了数据解析器并设置了所有的图形元素,比如轴、刻度标签和网格,您会发现从折线图切换到条形图非常容易。
绘制条形图
首先要改变的是计算xDelta
变量的方式。在这种情况下,x 轴失去了它的意义,只用于对不同部门的数据进行分组。HTML 表格有六个不同的日期,所以你把 x 轴分成六段。在折线图中,这六个日期与网格线相对应,给你五个部分。考虑到这些差异,让我们修改一下xDelta
变量,如清单 5-1 所示。
清单 5-1。ch5_01a.html
$(document).ready(function(){
...
table.find('tbody tr').each(function(i){
tableData.dataGroups[i] = [];
$(this).find('td').each(function(){
var tdVal = parseFloat( $(this).text() );
tableData.dataGroups[i].push( tdVal );
});
});
var xDelta = w / (tableData.xLabels.length);
var xlabelsUL = $('<ul class="labels-x"></ul>')
.width(w)
.height(h)
.insertBefore(canvas);
...
});
现在,删除清单 5-2 中用粗体显示的不再需要的行。
清单 5-2。ch5_01b.html
$(document).ready(function(){
...
//delete the following rows
ctx.lineWidth = 5;
for(var i in tableData.dataGroups){
var points = tableData.dataGroups[i];
ctx.moveTo(0,-points[i]);
ctx.strokeStyle = colors[i];
ctx.beginPath();
var xVal = margin.left;
for(var j in points){
var relY = (points[j]*h/tableData.maxVal) + 10;
ctx.lineTo(xVal,-relY);
xVal += xDelta;
}
ctx.stroke();
ctx.closePath();
}
//end delete
...
});
在它们的位置上,你现在可以写代码了(见清单 5-3)。
清单 5-3。ch5_01c.html
$(document).ready(function(){
...
$.each(tableData.yLabels, function(i){
var thisLi = $('<li><span>'+this+'</span></li>')
.prepend('<span class="line" />')
.css('bottom',liBottom*i)
.prependTo(ylabelsUL);
var label = thisLi.find('span:not(.line)');
var topOffset = label.height()/-2;
if(i == 0){ topOffset = -label.height(); }
else if(i== tableData.yLabels.length-1){ topOffset = 0; }
label
.css('margin-top', topOffset)
.addClass('label');
});
var barGroupMargin = 4;
for(var i in tableData.dataGroups){
ctx.beginPath();
var n = tableData.dataGroups.length;
var lineWidth = (xDelta - barGroupMargin * 2 ) / n;
var strokeWidth = lineWidth - (barGroupMargin * 2);
ctx.lineWidth = strokeWidth;
var points = tableData.dataGroups[i];
var xVal = (xDelta – n * strokeWidth - (n - 1) * (lineWidth - strokeWidth)) / 2;
for(var j in points){
var relX = margin.left + (xVal - barGroupMargin) + (i * lineWidth) + lineWidth / 2;
ctx.moveTo(relX, -margin.bottom);
var relY = margin.bottom + points[j] * h / tableData.maxVal;
ctx.lineTo(relX, -relY);
xVal += xDelta;
}
ctx.strokeStyle = colors[i];
ctx.stroke();
ctx.closePath();
}
var legendList = $('<ul class="legend"></ul>')
.insertBefore(canvas);
for(var i in tableData.legend){
$('<li>'+ tableData.legend[i] +'</li>')
.prepend('<span style="background: '+ colors[i] +'" />')
.appendTo(legendList);
}
...
});
因为您使用的是条形图而不是折线图,所以 x 轴会报告类别。这意味着 x 标签不应再出现在与记号对应的位置,而是出现在每个间隔的中心,由两个记号分隔。要快速简单地做到这一点,你必须编辑与 x 标签相关的margin-left
层叠样式表(CSS)属性,直接使用css()
jQuery 函数(见清单 5-4)。
清单 5-4。ch5_01d.html
$(document).ready(function(){
...
$.each(tableData.xLabels, function(i){
var thisLi = $('<li><span class="label">' + this + '</span></li>')
.prepend('<span class="line" />')
.css('left', xDelta * i)
.width(0)
.appendTo(xlabelsUL);
var label = thisLi.find('span.label');
label
.css('margin-left', '40px')
.addClass('label');
});
...
});
最终的条形图如图 5-1 所示。
图 5-1。
A bar chart representing the data in the table
摘要
在这一章中,你看到了使用你在前面章节中学到的知识来创建一个条形图是多么容易。在下一章中,您将实现另一种类型的图表,它既不使用网格,也不使用轴,而是使用圆形扇区:饼图。您还将了解如何插入动画来响应某些事件,如鼠标单击,以增加图表的交互性。
六、绘制饼图
Abstract
与你在前两章所做的相似,在这一章中,你将学习使用 HTML 表格中包含的数据来构建一个饼图。
与你在前两章所做的相似,在这一章中,你将学习使用 HTML 表格中包含的数据来构建一个饼图。
从你解析所有数据的地方开始(见第三章),你将发现如何实现这种有趣的对象,并探索如何创建你的第一个动画。
绘制饼图
这种类型的图表与前两种(折线图和条形图)有很大不同。它由一个圆圈(一个饼图)组成,代表所有值的总和(见图 6-1 )。这个饼图被分成不同颜色的切片,每个切片代表数据中出现的一个系列。切片的大小与系列中所有值的总和成比例,与饼图中所有值的总和成比例。因此,每个切片表示每个系列占其值总和的百分比。
图 6-1。
A pie chart
让我们看看如何实现这种图表的特殊功能。
设置画布
如前所述,在开发您的饼状图时,您将从在第三章结束时获得的代码开始,也就是说,从一个 HTML 表中包含的数据和实现的许多解析器中提取数据,并将它们放在不同的数组中。清单 6-1 显示了在第三章的结尾的代码状态。
清单 6-1。ch6_01.html
<HTML>
<HEAD>
<TITLE>MyChart</TITLE>
<style type="text/css">
table.myTable caption {
font-size: 14px;
padding-bottom: 5px;
font-weight: bold;
}
table.myTable {
font-family: verdana,arial,sans-serif;
font-size:11px;
color:#333333;
border-width: 1px;
border-color: #666666;
border-collapse: collapse;
}
table.myTable th {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666666;
background:#b5cfd2 url('img/cell-blue.jpg');
}
table.myTable td {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666666;
background:#dcddc0 url('img/cell-grey.jpg');
}
</style>
</HEAD>
<BODY>
<canvas id="myCanvas" width="500" height="400"> </canvas>
<table class="myTable">
<caption>Balloons Lost in Space</caption>
<thead>
<tr>
<td></td>
<th>May 2013</th>
<th>Jun 2013</th>
<th>Jul 2013</th>
<th>Aug 2013</th>
<th>Sep 2013</th>
<th>Oct 2013</th>
</tr>
</thead>
<tbody>
<tr>
<th>USA</th>
<td>12</td>
<td>40</td>
<td>75</td>
<td>23</td>
<td>42</td>
<td>80</td>
</tr>
<tr>
<th>Canada</th>
<td>3</td>
<td>22</td>
<td>40</td>
<td>27</td>
<td>35</td>
<td>21</td>
</tr>
<tr>
<th>Australia</th>
<td>60</td>
<td>80</td>
<td>16</td>
<td>28</td>
<td>33</td>
<td>26</td>
</tr>
<tr>
<th>Brazil</th>
<td>46</td>
<td>7</td>
<td>14</td>
<td>26</td>
<td>36</td>
<td>24</td>
</tr>
</tbody>
<tfoot>
<tr><td colspan="7">Data from Statistical Office of the Republic of Unhappy
Children</td></tr>
</tfoot>
</table>
<script src="
http://code.jquery.com/jquery-1.9.1.min.js
<script>
$(document).ready(function(){
var tableData = {};
var table = $('table');
tableData.xLabels = [];
table.find('thead th').each(function(){
tableData.xLabels.push( $(this).html() );
});
tableData.legend = [];
table.find('tbody th').each(function(){
tableData.legend.push( $(this).html() );
});
var tmp = [];
table.find('tbody td').each(function(){
var thisVal = parseFloat( $(this).text() );
tmp.push(thisVal);
});
if(Math.min.apply(null, tmp) > 0)
tableData.minVal = 0;
else
tableData.minVal = Math.min.apply(null, tmp);
tableData.maxVal = 1.1 * Math.max.apply(null, tmp);
tableData.yLabels = [];
var yDeltaPixels = 30;
var nTicks = Math.round(h / yDeltaPixels);
var yRange = tableData.maxVal - tableData.minVal;
var yDelta = Math.ceil(yRange / nTicks);
var yVal = tableData.minVal;
while( yVal < (tableData.maxVal - yDelta)){
tableData.yLabels.push(yVal);
yVal += yDelta;
}
tableData.yLabels.push(yVal);
tableData.yLabels.push(tableData.maxVal);
tableData.dataGroups = [];
table.find('tbody tr').each(function(i){
tableData.dataGroups[i] = [];
$(this).find('td').each(function(){
var tdVal = parseFloat( $(this).text() );
tableData.dataGroups[i].push( tdVal );
});
});
});
</script>
</BODY>
</HTML>
因此,让我们从画布上下文的定义开始。与折线图和条形图不同,您不需要翻译上下文,因为在饼图中没有轴来表示;取而代之的是定义画布的边距和大小(见清单 6-2)。注意传递到strokeRect()
中的值:y 值必须作为正值传递(没有转换)。
清单 6-2。ch6_01.html
$(document).ready(function(){
var canvas = $("#myCanvas");
var ctx = canvas.get(0).getContext("2d");
var colors = ['#be1e2d', '#666699', '#92d5ea', '#ee8310'];
var margin = {top: 30, right: 10, bottom: 10, left: 30},
w = canvas.width() - margin.left - margin.right,
h = canvas.height() - margin.top - margin.bottom;
ctx.strokeRect(margin.left,margin.top,w,h);
var tableData = {};
var table = $('table');
tableData.xLabels = [];
...
});
实现饼图
接下来,定义饼图的圆心(见清单 6-3)。假设它对应于绘图区域的中心,您定义两个变量,center_x
和center_y
,代表中心点的坐标,并从那里开始构建您的图表。一旦定义了一个pieMargin
来建立圆的边缘和边距之间的距离,就定义了半径。绘图区域的大小取决于pieMargin
(固定的)和圆心(动态的)之差。因此,半径将会改变,以适应绘图区域的大小。所有这些值都必须计算出来,并考虑到余量。
清单 6-3。ch6_01.html
$(document).ready(function(){
...
ctx.strokeRect(margin.left,margin.top,w,h);
var pieMargin = margin.top + 30;
var center_x = Math.round(w / 2) + margin.left;
var center_y = Math.round(h / 2) + margin.top;
var radius = center_y - pieMargin;
var counter = 0.0;
var tableData = {};
var table = $('table');
tableData.xLabels = [];
...
});
饼图表示表中写入的所有值的总和。因此,您定义了一个名为dataSum
的函数,然后定义了一个同名的变量,如清单 6-4 所示。
清单 6-4。ch6_01.html
$(document).ready(function(){
...
table.find('tbody tr').each(function(i){
tableData.dataGroups[i] = [];
$(this).find('td').each(function(){
var tdVal = parseFloat( $(this).text() );
tableData.dataGroups[i].push( tdVal );
});
});
var dataSum = function(){
var dataSum = 0;
for(var i in tableData.dataGroups){
var points = tableData.dataGroups[i];
for(var j in points){
dataSum += points[j];
}
}
return dataSum;
}
var dataSum = dataSum();
});
然后你写一个无序列表<ul>
标签,每个列表项由一个标签组成,显示在它对应的片旁边,如清单 6-5 所示。
清单 6-5。ch6_01.html
$(document).ready(function(){
...
var dataSum = dataSum();
var labels = $('<ul class="labels"></ul>')
.insertBefore(canvas);
});
现在,如清单 6-6 所示,您可以一个接一个地绘制饼图的切片。
清单 6-6。ch6_01.html
$(document).ready(function(){
...
var labels = $('<ul class="labels"></ul>')
.insertBefore(canvas);
for(var i in tableData.dataGroups){
var sum = 0;
var points = tableData.dataGroups[i];
for(var j in points){
sum += points[j];
}
var fraction = sum/dataSum;
ctx.beginPath();
ctx.moveTo(centerx, centery);
ctx.arc(centerx, centery, radius,
counter * Math.PI * 2 - Math.PI * 0.5,
(counter + fraction) * Math.PI * 2 - Math.PI * 0.5, false);
ctx.lineTo(centerx, centery);
ctx.closePath();
ctx.fillStyle = colors[i];
ctx.fill();
var sliceMiddle = (counter + fraction/2);
var distance = radius * 1.2;
var labelx = Math.round(centerx + Math.sin(sliceMiddle * Math.PI * 2) *
(distance));
var labely = Math.round(centery - Math.cos(sliceMiddle * Math.PI * 2) *
(distance));
var leftPlus = (labelx < centerx) ? '40' : '0' ;
var percentage = parseFloat((fraction*100).toFixed(2));
var labelval = percentage + "%";
var labeltext = $('<span class="label">' + labelval +'</span>')
.css('font-size', radius / 8)
.css('color', colors[i]);
var label = $('<li class="label-pos"></li>')
.appendTo(labels)
.css({left: labelx-leftPlus, top: labely})
.append(labeltext);
counter+=fraction;
}
});
让我们来分解一下清单:
- 您正在使用一个路径来绘制每个切片,使用
ctx.arc()
函数来说明圆的边缘。 counter
是与已经绘制的切片所覆盖的百分比直接相关的累积值,并且是从 0 到 1 的值。当该值为 1 时,您已经绘制了 100%的饼图。- 变量
fraction
是每个单切片覆盖的百分比,对应的百分比存储在同名变量:percentage
中。 - 接下来,定义
sliceMiddle
变量,它代表每个切片的平分线的角度(百分比)。您使用sliceMiddle
值将标签放置在正确的角度,正好在切片的中间。 - 您选择将报告百分比的标签放在切片的外面(但是没有什么可以阻止您在里面表示它们)。
distance
变量是你决定放置标签的距离饼图的中心有多远。在这里,选择一个对应于半径 120%的值。 - 每个标签都作为一个动态的
<li>
标签写在网页中,字体大小与饼图的大小成比例,颜色与国家(系列)相对应。
如果你想看看这些动态生成的行是如何构造的,你可以使用 Firebug 或者 DevTools(见第一章),从菜单中选择 HTML 标签。图 6-2 显示了您刚刚动态生成的特定结构。
图 6-2。
Using Firebug, it is possible to see the HTML structure dynamically generated
完成饼图
要完成您的饼图,您需要添加标题。你可以直接从表中选择caption
,如清单 6-7 所示。
清单 6-7。ch6_01.html
$(document).ready(function(){
...
for(var i in tableData.dataGroups){
...
}
$('<div class="chart-title">'+table.find('caption').html() + '</div>')
.insertBefore(canvas);
});
添加到图表中的最后但并非最不重要的元素是图例。这对于这种类型的图表尤其重要,因为没有对与切片颜色相对应的数据系列的引用。因此,您必须添加这个元素,如清单 6-8 所示。
清单 6-8。ch6_01.html
$(document).ready(function(){
...
$('<div class="chart-title">'+table.find('caption').html() + '</div>')
.insertBefore(canvas);
var legendList = $('<ul class="legend"></ul>')
.insertBefore(canvas);
for(var i in tableData.legend){
$('<li>'+ tableData.legend[i] +'</li>')
.prepend('<span style="background: '+ colors[i] +'" />')
.appendTo(legendList);
}
});
此外,您已经添加了许多在级联样式表(CSS)样式语句中引用的类(参见清单 6-9)。此外,您必须将画布放在页面的顶部,因此您需要将表格放在画布下面,设置table.myTable
类的top
和left
CSS 属性。
清单 6-9。ch6_01.html
<style>
...
table.myTable {
font-family: verdana,arial,sans-serif;
font-size:11px;
color:#333333;
border-width: 1px;
border-color: #666666;
border-collapse: collapse;
position: fixed;
top: 450px;
left: 20px;
}
...
ul, .li {
margin: 0;
padding: 0;
}
.labels {
list-style: none;
}
.label-pos, label {
position: absolute;
margin-left: 0px;
margin-top: 0px;
padding:0;
}
.label { display: block;
color: #fff;
font-weight: bold;
font-size: 1em;
}
.chart-title {
font-size: 24;
font-weight: bold;
position: absolute;
left: 150px;
top: 10px;
width: 100%;
}
.legend {
list-style: none;
position: absolute;
left:520;
top: 40;
border: 1px solid #000;
padding: 10px;
}
.legend li span {
width: 12px;
height: 12px;
float: left;
margin: 3px;
}
</style>
最后,你得到了如图 6-3 所示的饼状图。
图 6-3。
The pie chart representing the data in the table
添加效果
现在您已经获得了您的饼图,您将进一步研究,学习如何通过添加有趣的效果来改善图表的外观。您将看到如何通过添加渐变来控制切片的颜色。您还将了解如何制作切片动画,以创建交互式饼图。
添加渐变效果
您用统一颜色填充的切片绘制了您的饼图,但您可以进行一些更改。例如,您可以给切片添加渐变效果。为此,您需要替换
ctx.fillStyle = colors[i];
清单 6-10 中的行。
清单 6-10。ch6_02.html
$(document).ready(function(){
...
ctx.beginPath();
ctx.moveTo(center_x, center_y);
ctx.arc(center_x, center_y, radius,
counter * Math.PI * 2 - Math.PI * 0.5,
(counter + fraction) * Math.PI * 2 - Math.PI * 0.5, false);
ctx.lineTo(center_x, center_y);
ctx.closePath();
var sliceGradientColor = "#ddd";
var sliceGradient = ctx.createLinearGradient( 0, 0, w, h );
sliceGradient.addColorStop( 0, sliceGradientColor );
sliceGradient.addColorStop( 1, colors[i]);
ctx.fillStyle = sliceGradient;
ctx.fill();
var sliceMiddle = (counter + fraction/2); ...
});
出于这些例子的目的,不再需要显示 HTML 表格,所以你可以隐藏它,选择它,然后链接hide()
函数(见清单 6-11)。
清单 6-11。ch6_02.html
$(document).ready(function(){
...
$('table').hide();
});
正如你在图 6-4 中所看到的,你选择了一种白色作为与现有颜色重叠的渐变颜色,但是你也可以通过在sliceGradientColor
中写入红绿蓝(RGB)十六进制来选择任何颜色。
图 6-4。
Adding a white gradient to your pie chart
添加更好的渐变效果
但是,老实说,你对你的结果不是很满意,所以你做了额外的改变来获得更好的渐变效果(见清单 6-12)。这一次,您选择了一种深色,几乎是黑色,作为颜色渐变,将非常深的灰色分配给sliceGradientColor
变量。此外,您可以通过创建分割空间来分隔切片,从而进一步增强渐变效果。一个更好的选择是绘制中间的空间,创建一个白色边框,而不是将切片分开。因此,您定义了sliceBorderWidth
变量,通过它您可以调整分割大小,以及sliceBorderStyle
变量,来设置白色。
清单 6-12。ch6_03.html
$(document).ready(function(){
...
ctx.beginPath();
ctx.moveTo(center_x, center_y);
ctx.arc(center_x, center_y, radius,
counter * Math.PI * 2 - Math.PI * 0.5,
(counter + fraction) * Math.PI * 2 - Math.PI * 0.5, false);
ctx.lineTo(center_x, center_y);
ctx.closePath();
var sliceGradientColor = "#222";
var sliceBorderStyle = "#fff";
var sliceBorderWidth = 4;
var sliceGradient = ctx.createLinearGradient(0, 0, w*.7, h*.7);
sliceGradient.addColorStop(0, sliceGradientColor);
sliceGradient.addColorStop(1, colors[i]);
ctx.fillStyle = sliceGradient;
ctx.fill();
ctx.lineWidth = sliceBorderWidth;
ctx.strokeStyle = sliceBorderStyle;
ctx.stroke();
var sliceMiddle = (counter + fraction / 2);
...
});
图 6-5 显示了相同的饼状图,但是带有黑色渐变,使切片更有深度。
图 6-5。
Adding a black gradient and space divisions to your pie chart
创建拉出一个切片的饼图
人们通常会吃馅饼,为了诱惑你这样做,面包师通常会从馅饼的剩余部分切下一片,以便更好地展示他或她的商品。玩笑归玩笑,如果在一个饼图中表示的不同系列中,你想突出显示某个特定的系列,有时相关的切片会被抽出来。
现在,假设你对第三个系列感兴趣;你可以把这一片拉出来。为此,首先你需要在通过切片的循环中添加两个新变量:startAngle
和endAngle
,如清单 6-13 所示。
清单 6-13。ch6_04.html
$(document).ready(function(){
...
for(var i in tableData.dataGroups){
var sum = 0;
var points = tableData.dataGroups[i];
for(var j in points){
sum += points[j];
}
var fraction = sum / dataSum;
var startAngle = counter * Math.PI * 2 - Math.PI * 0.5;
var endAngle = (counter + fraction) * Math.PI * 2 - Math.PI * 0.5;
ctx.beginPath();
ctx.moveTo(center_x, center_y);
...
}
...
});
在for()
循环中,你必须编写仅对第三个片有效的代码——也就是说,当索引i
为 2 时(见清单 6-14)。
清单 6-14。ch6_04.html
$(document).ready(function(){
...
for(var i in tableData.dataGroups){
var sum = 0;
var points = tableData.dataGroups[i];
for(var j in points){
sum += points[j];
}
var fraction = sum / dataSum;
var startAngle = counter * Math.PI * 2 - Math.PI * 0.5;
var endAngle = (counter + fraction) * Math.PI * 2 - Math.PI * 0.5;
if(i == 2){
var currentPullOutDistance = 20;
var maxPullOutDistance = 25;
var ratio = currentPullOutDistance/maxPullOutDistance;
var midAngle = (startAngle + endAngle) / 2;
var actualPullOutDistance = currentPullOutDistance *
(Math.pow( 1 - ratio, .8 ) + 1);
var startx = centerx + Math.cos(midAngle) * actualPullOutDistance;
var starty = centery + Math.sin(midAngle) * actualPullOutDistance;
ctx.beginPath();
ctx.moveTo(startx, starty);
ctx.arc(startx, starty, radius, startAngle,endAngle, false);
ctx.lineTo(startx, starty);
ctx.closePath();
}else{
ctx.beginPath();
ctx.moveTo(centerx, centery);
ctx.arc(centerx, centery, radius, startAngle,endAngle, false);
ctx.lineTo(centerx, centery);
ctx.closePath();
}
var sliceGradientColor = "#222";
var sliceBorderStyle = "#fff";
var sliceBorderWidth = 4;
...
}
...
});
图 6-6 显示了一个切片被拉出的图表。
图 6-6。
The pie chart with a slice pulled out
在这个例子中,您已经看到了如何从馅饼中取出一块。这是你为自己设定的目标的第一步,也就是让你的饼图具有交互性。下一步是创建一个动画,您可以在其中看到从图表中提取的切片。
插入动画以拉出切片
有了 JavaScript,除了你的想象力之外,你可以对你的图表做任何改变。对你的图表的一个额外的触摸可能是创建一个动画。例如,当页面刚刚被加载时,您可以完整地显示饼图,然后逐渐拉出您想要用动画突出显示的部分。
您正在开发的代码逐渐变得更加复杂;因此,有必要开始根据代码行的功能将它们分组在一起,从而避免重复和可读性较差的代码。让我们从渐变效果的代码开始。首先,编写sliceGradient()
函数,它管理颜色渐变效果。该函数只接受一个参数:重叠切片的颜色渐变。每当您想要对画布应用渐变效果时,该函数都会返回分配给画布二维上下文的值。在函数之后,让我们从for()
循环中取出sliceBorderStyle
和sliceBorderWidth
变量的定义。
清单 6-15。ch6_05.html
$(document).ready(function(){
...
var dataSum = dataSum();
var labels = $('<ul class="labels"></ul>')
.insertBefore(canvas);
function sliceGradient(color){
var sliceGradientColor = "#222";
var sliceGradient = ctx.createLinearGradient(0, 0, w * .7, h * .7);
sliceGradient.addColorStop(0, sliceGradientColor);
sliceGradient.addColorStop(1, color);
return sliceGradient;
}
var sliceBorderStyle = "#fff";
var sliceBorderWidth = 4;
for(var i in tableData.dataGroups){
var sum = 0;
var points = tableData.dataGroups[i];
...
}
...
});
你想开发的另一个函数是fraction()
(见清单 6-16)。此函数计算 0 到 1 之间的值,该值表示切片相对于整个饼图的分数。正如您将看到的,在许多情况下,返回值是有用的。
清单 6-16。ch6_05.html
$(document).ready(function(){
...
var sliceBorderStyle = "#fff";
var sliceBorderWidth = 4;
function fraction(i) {
var sum = 0;
var points = tableData.dataGroups[i];
for(var j in points){
sum += points[j];
}
return (sum/dataSum);
}
for(var i in tableData.dataGroups){
var sum = 0;
var points = tableData.dataGroups[i];
...
}
...
});
现在您收集了整个函数序列,您用上下文ctx
调用它,如清单 6-17 所示。如果参数化正确,这个序列总是相同的,所以您可以在一个将被称为drawSlice()
的函数中收集它。
清单 6-17。ch6_05.html
$(document).ready(function(){
...
function fraction(i) {
var sum = 0;
var points = tableData.dataGroups[i];
for(var j in points){
sum += points[j];
}
return (sum/dataSum);
}
function drawSlice(centerx, centery, radius, counter, i) {
var startAngle = counter * Math.PI * 2 - Math.PI * 0.5;
var endAngle = (counter + fraction(i)) * Math.PI * 2 - Math.PI * 0.5;
ctx.beginPath();
ctx.moveTo(centerx, centery);
ctx.arc(centerx, centery, radius, startAngle,endAngle, false);
ctx.lineTo(centerx, centery);
ctx.closePath();
ctx.fillStyle = sliceGradient(colors[i]);
ctx.fill();
ctx.lineWidth = sliceBorderWidth;
ctx.strokeStyle = sliceBorderStyle;
ctx.stroke();
}
for(var i in tableData.dataGroups){
var sum = 0;
var points = tableData.dataGroups[i];
...
}
...
});
您可以将相同的逻辑应用到用于生成 HTML 无序列表<ul>
标签的代码中,如清单 6-18 所示。
清单 6-18。ch6_05.html
$(document).ready(function(){
...
function drawSlice(centerx,centery,radius,counter,i) {
...
ctx.lineWidth = sliceBorderWidth;
ctx.strokeStyle = sliceBorderStyle;
ctx.stroke();
}
function drawLabels(i, counter) {
var sliceMiddle = (counter + fraction(i)/2);
var distance = radius * 1.2;
var labelx = Math.round(centerx + Math.sin(sliceMiddle * Math.PI * 2) *
(distance));
var labely = Math.round(centery - Math.cos(sliceMiddle * Math.PI * 2) *
(distance));
var leftPlus = (labelx < centerx) ? '40' : '0' ;
var percentage = parseFloat((fraction(i)*100).toFixed(2));
var labelval = percentage + "%";
var labeltext = $('<span class="label">' + labelval +'</span>')
.css('font-size', radius / 8)
.css('color', colors[i]);
var label = $('<li class="label-pos"></li>')
.appendTo(labels)
.css({left: labelx-leftPlus, top: labely})
.append(labeltext);
}
for(var i in tableData.dataGroups){
var sum = 0;
var points = tableData.dataGroups[i];
...
}
...
});
在这些语句之后,代码中处理切片循环的部分被缩减为清单 6-19 所示。
清单 6-19。ch6_05.html
$(document).ready(function(){
...
function drawLabels(i, counter) {
...
}
for(var i in tableData.dataGroups){
if(i == 2){
counterAtI2 = counter;
}else{
drawSlice(centerx, centery, radius, counter, i);
}
drawLabels(i, counter);
counter += fraction(i);
}
$('<div class="chart-title">'+table.find('caption').html() + '</div>')
.insertBefore(canvas);
...
});
正如您所看到的,代码的可读性大大提高了,在必要的地方进行修改也更加容易了。在您正确地分离了代码的各个部分之后,您将继续处理动画的代码。动画的核心是setInterval()
函数(见清单 6-20)。这个函数有两个参数:第一个是实现要执行的操作的函数,第二个是作为第一个参数的函数每次执行之间的时间间隔长度(以毫秒为单位)。因此,您定义了next()
函数,对于每一次执行,它将把切片画得离中心越来越远,直到它到达maxPullOutDistance
。所以你需要一个计数器k
,它会一步步增加,直到达到一个最大值,在这个最大值中,函数的每次执行都会在同一个地方绘制切片,让它看起来就像动画完成了一样(实际上它一直在运行)。
清单 6-20。ch6_05.html
$(document).ready(function(){
...
for(var i in tableData.dataGroups){
if(i == 2){
counterAtI2 = counter;
}else{
drawSlice(centerx,centery,radius,counter,i);
}
drawLabels(i,counter);
counter+=fraction(i);
}
var nextMove = setInterval(next, 100);
var k = 0;
function next() {
var midAngle = Math.PI * (2 * counterAtI2 + fraction(2) - 0.5);
var currentPullOutDistance = k;
var maxPullOutDistance = 45;
var ratio = currentPullOutDistance / maxPullOutDistance;
var actualPullOutDistance = currentPullOutDistance * (Math.pow(1 - ratio, .8) + 1);
var startx = centerx + Math.cos(midAngle) * actualPullOutDistance;
var starty = centery + Math.sin(midAngle) * actualPullOutDistance;
drawSlice(startx, starty, radius, counterAtI2, 2);
if(k < maxPullOutDistance){
k++;
}else{
k = maxPullOutDistance;
clearInterval(nextMove);
}
}
$('<div class="chart-title">'+table.find('caption').html() + '</div>')
.insertBefore(canvas);
...
});
如果你加载网页,你可以看到你的动画,如图 6-7 从左到右所示。
图 6-7。
Different frames of the animation while the slice is being pulled away from the pie chart
点按切片以将其拉出
在前面的例子中,您看到了如何实现动画。在这个例子和下面的例子中,你将采取进一步的步骤:添加作为特定事件的结果来启动动画的能力。最常见的事件类型,也是最适合饼图的事件类型,是单击特定的切片。。单击后,切片将直接从饼图中拉出。您还将学习当单击另一个切片时,如何处理切片返回到饼图。
你编写一个新的slice()
函数,它将构建一个切片对象及其所有属性,比如startAngle
、endAngle
、counter
和fraction
(见清单 6-21)。到目前为止,你已经含蓄地做到了这一点,但这种方式更正确。您还定义了sliceAll
数组,它将包含饼图的所有切片。
清单 6-21。ch6_06.html
$(document).ready(function(){
...
function drawLabels(i, counter) {
...
}
function slice(counter,i){
var startAngle = counter * Math.PI * 2 - Math.PI * 0.5;
var endAngle = (counter + fraction(i)) * Math.PI * 2 - Math.PI * 0.5;
this.startAngle = startAngle;
this.endAngle = endAngle;
this.counter = counter;
this.fraction = fraction(i);
return this;
}
var allSlices = new Array();
for(var i in tableData.dataGroups){
...
}
...
});
使用新的slice()
函数,通过添加切片的定义来稍微修改for()
循环,如清单 6-22 所示。
清单 6-22。ch6_06.html
$(document).ready(function(){
...
var allSlices = new Array();
for(var i in tableData.dataGroups){
allSlices[i] = new slice(counter, i);
drawSlice(centerx, centery, radius, counter, i);
drawLabels(i, counter);
counter += fraction(i);
}
var nextMove = setInterval(next, 100);
var k = 0;
...
});
在继续之前,让我们把全局变量添加到代码中(见清单 6-23)。同时,必须删除或注释掉nextMove
和k
变量。
清单 6-23。ch6_06.html
$(document).ready(function(){
...
for(var i in tableData.dataGroups){
allSlices[i] = new slice(counter,i);
drawSlice(centerx,centery,radius,counter,i);
drawLabels(i,counter);
counter+=fraction(i);
}
var sliceToPullout = -1;
var sliceToPullin = 0;
//var nextMove = setInterval(next, 100);
//var k = 0;
function next() {
...
}
...
});
接下来,在画布上激活鼠标click
事件的捕获,并将其链接到一个将调用handleChartClick()
的函数,如清单 6-24 所示。
清单 6-24。ch6_06.html
$(document).ready(function(){
...
function next() {
...
}
$('#myCanvas').click(handleChartClick);
$('<div class="chart-title">'+table.find('caption').html() + '</div>')
.insertBefore(canvas);
$('table').hide();
});
现在,你必须实现handleChartClick()
函数,你刚刚把它作为参数插入到click()
函数中(见清单 6-25)。mouseX
和mouseY
变量存储你点击鼠标按钮时画布上的点的坐标。分析这些值以确定它们是否对应于饼图面以及它们对应于哪个切片。一旦切片被识别,它就被标记为传出,而当前拉出的切片被标记为传入。然后,抛出next()
函数。这实现了要在画布上采取的动作。
此外,您必须考虑点击的切片是提取的切片的可能性。在这种情况下,当所有切片都在饼图中时,您将返回到初始状态。
清单 6-25。ch6_06.html
$(document).ready(function(){
...
function next() {
...
}
function handleChartClick ( clickEvent ) {
var mouseX = clickEvent.pageX - this.offsetLeft;
var mouseY = clickEvent.pageY - this.offsetTop;
var xFromCentre = mouseX - centerx;
var yFromCentre = mouseY - centery;
var distanceFromCentre =
Math.sqrt( Math.pow( Math.abs( xFromCentre ), 2 ) +
Math.pow( Math.abs( yFromCentre ), 2 ) );
if ( distanceFromCentre <= radius ) {
var clickAngle = Math.atan2( yFromCentre, xFromCentre );
if(yFromCentre < 0``&&
clickAngle = (Math.PI + clickAngle) + Math.PI;
for ( var i in allSlices ) {
if ( clickAngle >= allSlices[i].startAngle
&&
clickAngle <= allSlices[i].endAngle ) {
sliceToPullin = sliceToPullout;
sliceToPullout = i;
if(sliceToPullout == sliceToPullin)
sliceToPullout = -1;
next(sliceToPullout,sliceToPullin);
}
}
}
}
$('#myCanvas').click ( handleChartClick );
$('<div class="chart-title">'+table.find('caption').html() + '</div>')
.insertBefore(canvas);
$('table').hide();
});
你用一个新的带两个参数的函数来替换已经存在的next()
:out
和ins
(见清单 6-26)。每个切片都用一个编号来标识,该编号对应于序列中循环的索引。Out
和ins
是变量,存储关于必须从饼图中取出哪个切片以及必须重新插入哪个切片的信息。只有当一个切片被点击时,事件才被捕获,并抛出next()
函数。它做的第一件事是用clearRect()
函数清除整个画布。当循环通过所有切片时,它们被一个接一个地绘制出来。如果切片的索引等于变量out
,切片将被拉出饼图,但是如果它的索引等于ins
,它将被拉入饼图。
清单 6-26。ch6_06.html
$(document).ready(function(){
...
function next(out,ins) {
ctx.clearRect ( 0, 0, canvas.width(), canvas.height() );
ctx.lineWidth = 1;
ctx.strokeStyle = '#000';
ctx.strokeRect(margin.left,margin.top,w,h);
for(var i in allSlices){
var counter = allSlices[i].counter;
var startAngle = allSlices[i].startAngle;
var endAngle = allSlices[i].endAngle;
var fraction = allSlices[i].fraction;
var maxPullOutDistance = 30;
if( i == out){
//Pull out
var currentPullOutDistance = 30;
var ratio = currentPullOutDistance / maxPullOutDistance;
var midAngle = (startAngle + endAngle) / 2;
var actualPullOutDistance =
currentPullOutDistance * (Math.pow( 1 - ratio, .8 ) + 1);
var startx = centerx + Math.cos(midAngle) * actualPullOutDistance;
var starty = centery + Math.sin(midAngle) * actualPullOutDistance;
drawSlice(startx, starty, radius, counter, i);
}else if(i == ins){
//Push In
var currentPullOutDistance = 0;
var ratio = currentPullOutDistance / maxPullOutDistance;
var midAngle = (startAngle + endAngle) / 2;
var actualPullOutDistance = currentPullOutDistance *
(Math.pow( 1 - ratio, .8 ) + 1);
var startx = centerx + Math.cos(midAngle) * actualPullOutDistance;
var starty = centery + Math.sin(midAngle) * actualPullOutDistance;
drawSlice(startx, starty, radius, counter, i);
}else{
drawSlice(centerx, centery, radius, counter, i);
}
}
}
function handleChartClick ( clickEvent ) {
...
}
...
});
现在,当你加载网页时,你会得到一个交互式饼图(见图 6-8 )!
图 6-8。
The user can choose which slice to extract from the pie
单击切片以动画形式将其拉出
既然您已经了解了如何将一个事件连接到一个函数,那么您必须确保鼠标点击激活一个动画。首先,添加两个计数器,如清单 6-27 所示。这些用于存储传出和传入切片从饼图中心覆盖的步长(距离)。当一个计数器处于最大距离时,另一个必须处于 0。
清单 6-27。ch6_06.html
$(document).ready(function(){
...
var sliceToPullout = -1;
var sliceToPullin = 0;
var k1 = 0;
var k2 = 20;
function next() {
...
}
...
});
您可以同时插入两个动画,一个处理传入切片,另一个处理传出切片。为了实现这一点,您调用两个不同的setInterval()
函数;一个是当索引i
匹配out
值时,另一个是当i
匹配ins
值时(见清单 6-28)。您还必须定义两个不同的函数来描述两个不同的动作,以便您可以将它们作为参数传递给两个setInterval()
函数。
清单 6-28。ch6_06.html
$(document).ready(function(){
...
function next(out,ins) {
ctx.clearRect ( 0, 0, canvas.width(), canvas.height() );
ctx.lineWidth = 1;
ctx.strokeStyle = '#000';
ctx.strokeRect(margin.left,margin.top,w,h);
for(var i in allSlices){
var counter = allSlices[i].counter;
var startAngle = allSlices[i].startAngle;
var endAngle = allSlices[i].endAngle;
var fraction = allSlices[i].fraction;
var maxPullOutDistance = 25;
if( i == out){
var nextMove = setInterval(pullOut, 100);
}else if(i == ins){
var nextMove = setInterval(pushIn, 100);
}else{
drawSlice(centerx, centery, radius, counter, i);
}
}
}
function handleChartClick ( clickEvent ) {
...
}
...
});
现在,你必须实现pullout()
函数(见清单 6-29)。与前面的例子相比,这里没有什么新的东西,除了在最后,您需要管理k1
计数器。
清单 6-29。ch6_06.html
$(document).ready(function(){
...
function next() {
...
}
function pullOut(){
var s = sliceToPullout;
var counter = allSlices[s].counter;
var startAngle = allSlices[s].startAngle;
var endAngle = allSlices[s].endAngle;
var fraction = allSlices[s].fraction;
var maxPullOutDistance = 25;
var currentPullOutDistance = k1;
var ratio = currentPullOutDistance / maxPullOutDistance;
var midAngle = (startAngle + endAngle) / 2;
var actualPullOutDistance = currentPullOutDistance * (Math.pow( 1 - ratio, .8 ) + 1);
var startx = centerx + Math.cos(midAngle) * actualPullOutDistance;
var starty = centery + Math.sin(midAngle) * actualPullOutDistance;
drawSlice(startx, starty, radius, counter, s);
if(k1 < 20){
k1++;
}else{
k1 = 20;
clearInterval(nextMove);
}
}
function handleChartClick ( clickEvent ) {
...
}
...
});
这同样适用于pushIn()
函数,如清单 6-30 所示。
清单 6-30。ch6_06.html
$(document).ready(function(){
...
function pullOut() {
...
}
function pushIn(){
var s = sliceToPullin;
var counter = allSlices[s].counter;
var startAngle = allSlices[s].startAngle;
var endAngle = allSlices[s].endAngle;
var fraction = allSlices[s].fraction;
var maxPullOutDistance = 25;
var currentPullOutDistance = k2;
var ratio = currentPullOutDistance / maxPullOutDistance;
var midAngle = (startAngle + endAngle) / 2;
var actualPullOutDistance = currentPullOutDistance * (Math.pow( 1 - ratio, .8 ) + 1);
var startx = centerx + Math.cos(midAngle) * actualPullOutDistance;
var starty = centery + Math.sin(midAngle) * actualPullOutDistance;
drawSlice(startx, starty, radius, counter, s);
if(k2 > 0){
k2--;
}else{
k2 = 0;
clearInterval(nextMove);
}
}
function handleChartClick ( clickEvent ) {
...
}
...
});
对于事件处理函数handleChartClick()
,你需要重置k1
和k2
计数器的值,这样每次你点击一个切片,一个新的动画就会开始(见清单 6-31)。
清单 6-31。ch6_06.html
$(document).ready(function(){
...
function handleChartClick ( clickEvent ) {
...
if ( distanceFromCentre <= radius ) {
var clickAngle = Math.atan2( yFromCentre, xFromCentre );
if(yFromCentre < 0 && xFromCentre < 0)
clickAngle = (Math.PI + clickAngle) + Math.PI;
for ( var i in allSlices ) {
if ( clickAngle >= allSlices[i].startAngle &&
clickAngle <= allSlices[i].endAngle ) {
sliceToPullin = sliceToPullout;
sliceToPullout = i;
if(sliceToPullout == sliceToPullin)
sliceToPullout = -1;
k1 = 0;
k2 = 20;
next(sliceToPullout,sliceToPullin);
}
}
}
}
...
});
在这个例子中,您可以看到用交互式动画实现图表的可能性,这些动画对用户触发的事件做出反应。
其他影响
在整本书中,你会发现你可以添加到你的图表中的其他效果,你会看到它们中的许多已经在图表表示的专门库中实现了。
在浏览器上绘制图表的一个效果是,组成图表的元素一个接一个地绘制,而不是一次全部绘制。这种效果会导致动画波动。将详细讨论的另一个效果是突出显示表示数据的元素(如切片、条形或线上的数据点)。当用户将鼠标放在其中一个项目上时,图表可能会显示一些小动画,例如更改元素的形状或颜色,或者创建一个包含附加信息的小框(工具提示)。
摘要
通过这一章,你已经学习了三种最常见的图表的开发,使用了从 HTML 表格中获得的相同的一组数据。您还看到了可以通过插入动画来丰富您的图表,以响应用户触发的某些事件,例如单击饼图中的一个切片将其拉出图表。
在下一章,你将读到本书第一部分的结尾。您将会发现,您在最后几章中分别实现的所有功能是如何合并到一个模块中的:一个生成图表的 JavaScript 库。你将逐渐理解这些类型的库是如何工作的——你将在本书的其他部分详细研究这些库。
七、为简单图表创建库
Abstract
作为这本书第一部分的结论,你将利用你目前所学的一切从头开始创建一个你自己的图书馆。这将是一个专门表示三种不同类型的图表的库,这三种不同类型的图表是:折线图、条形图和饼图。
作为这本书第一部分的结论,你将利用你目前所学的一切从头开始创建一个你自己的图书馆。这将是一个专门表示三种不同类型的图表的库,这三种不同类型的图表是:折线图、条形图和饼图。
您将开发的是一个非常简化的当前互联网上可用的 JavaScript 库模型。目的是帮助您理解这种图表表示专用库的机制。
通过在这个简单的例子中包含所有的步骤,您可以更好地了解这个类的库是如何工作的,即使对于复杂得多的例子也是如此。跟随数据的流动,从它们在 HTML 页面中的定义到它们在库中的处理,您将发现数据是如何被转换成图形元素以形成您最感兴趣的图表类型的。这个库的逐步实现将阐明为什么 jQuery 库是许多此类库的基础。由于它的功能,可以动态管理 HTML 页面的组件。该库还在许多参数的实现和管理中起着关键作用,这些参数将对所创建的图形元素的属性产生直接影响,从而在不修改代码的情况下表征不同的图表表示,每次都指定一个 JavaScript 对象,在该对象中将传递所有这些参数。
在前几章中,您已经开发了这里需要的代码。由于画布的上下文和 jQuery 提供的许多功能,您已经看到了如何迭代地管理数据,如何将数据转换成图形。这样,您创建了三种最常见的图表类型。您将使用您开发的代码来创建您的库,探索如何将其参数化,以便您可以决定表示哪个图表以及在调用库时如何表示,而无需修改代码。
创建库
首先,您需要定义您的新库,并将其包含在您的 web 页面中。为此,您创建一个包含myLibrary()
函数定义的新文件。您将该文件另存为myLibrary.js
;这将是你的库,只要你想把它包含在一个网页中,它就可以被重用(见清单 7-1)。
清单 7-1。myLibrary.js
function myLibrary(target, data, options){
//add the JavaScript code here
}
同时,您开始实现一个新的 web 页面,包括其中的myLibrary
文件,如清单 7-2 所示。
清单 7-2。ch7_01.html
<HTML>
<HEAD>
<TITLE>MyChart</TITLE>
</HEAD>
<BODY>
<script type="text/javascript" src="../src/jquery.min.js"></script>
<script type="text/javascript" src="./mylibrary.js"></script>
<script>
$(document).ready(function(){
// add data and options here
myLibrary("#myCanvas", data, options);
});
</script>
<canvas id="myCanvas" width="500" height="400"> </canvas>
</BODY>
</HTML>
正如您所看到的,首先您已经在 web 页面中包含了 jQuery 库,这样您就可以在使用代码时利用这个库提供的所有方法。
Note
如果您希望将 jQuery 库包含在内容交付网络(CDN)服务中,您需要使用以下参考资料:
<script src="
http://code.jquery.com/jquery-1.9.1.min.js
有关工作区和库路径的更多信息,请参见附录 a。
主要功能:目标、数据和选项
在$(document).ready()
函数中,你用myLibrary()
函数调用你的库。在这个调用中,您传递了三个不同的参数:
target
data
options
你会发现这种类型的调用在很多库中都很常见,包括 jqPlot 和 Highcharts。因此,在实现您的库时,您已经开始处理将构成后续章节基础的概念。
target
是画布的 ID 类的名称。target
被传递到库中,使您能够定义一个上下文并在其中绘制所有需要的图形。您可以在同一个网页中使用不同的画布,每个画布代表一种不同的图表类型,但是必须通过给target
起不同的名字来区分所有画布。data
是包含输入数据的数组。在前面的章节中,您使用了包含在表中的数据,并通过您实现的解析器将它提取出来。这有助于您理解 jQuery 的潜力,但是,事实上,大多数时候这些输入值可以有任何来源并采用任何形式。通常,转换成可读格式不是库的工作,而是其他支持代码的工作。因此,对于您的库,输入格式必须是数组。options
是一种对象数据类型,也可以采用复杂的结构,为此你需要指定一些与属性值相关的属性。您将使用这种类型的结构将一系列参数传递给库,以描述图表的所有图形组件。基本上,这需要定义一套关于你希望你的库如何表示你的图表的指导方针。
一旦你熟悉了这些基本概念,你会发现这本书涵盖的所有库,以及互联网上的其他库,将会更容易理解和使用。
与所有其他库一样,您正在实现的库将接受数组形式的输入数据。您不用实现解析器,解析器从 HTML 表中提取值,而是以数字数组的形式直接写入数据。在调用myLibrary()
函数之前,data
变量将在$(document).ready()
中定义(见清单 7-3)。
清单 7-3。ch7_01.html
$(document).ready(function(){
var data = [[12, 40, 75, 23, 42, 80],
[3, 22, 40, 27, 35, 21],
[60, 80, 16, 28, 33, 26],
[46, 7, 14, 26, 36, 24]];
myLibrary("#myCanvas",data,options);
});
如前所述,这些是 HTML 表格的数值(见图 7-1 )。但是,标题、月份和国家名称发生了什么变化呢?您也将把这两组值表示为一个数组,但是您没有将它们作为输入数据引入,而是将它们用作通过options
插入的图表的属性。
图 7-1。
The input data can come from any kind of source (e.g., a table); the important thing to keep the data structure
您可以考虑一年中的月份刻度标签(也称为类别),但是对于国家的名称,它们只不过是一系列值的名称,这些值将在图例中报告,并且将被分配不同的颜色。事实上,大多数标签将被分配给图表的组件,所以最好通过options
传递它们(见清单 7-4)。以前,您通过数组定义了一个颜色序列。因此,您也将在options
中传递这个数组。
清单 7-4。ch7_01.html
$(document).ready(function(){
var data = [[12, 40, 75, 23, 42, 80],
[3, 22, 40, 27, 35, 21],
[60, 80, 16, 28, 33, 26],
[46, 7, 14, 26, 36, 24]];
var options = {
categories: ["May 2012", "Jun 2012", "Jul 2012",
"Aug 2012", "Sep 2012", "Oct 2012"],
series: ["USA","Canada","Australia", "Brazil"],
colors: ['#be1e2d', '#666699', '#92d5ea', '#ee8310'],
};
myLibrary("#myCanvas",data,options);
});
但是,你忘了最重要的一点。您想使用哪种类型的图表?您也将在options
中指定这个信息,在这个特殊的例子中,定义一个具有三个可能值的type
属性:
line
bar
pie
就图表中可以参数化的内容而言,这只是冰山一角。在实现你的库时,任何表征图形元素外观或功能的参数都可以通过options
对象在外部设置,如清单 7-5 所示。
清单 7-5。ch7_01.html
$(document).ready(function(){
var data = [[12, 40, 75, 23, 42, 80],
[3, 22, 40, 27, 35, 21],
[60, 80, 16, 28, 33, 26],
[46, 7, 14, 26, 36, 24]];
var options = {
//type: 'line',
type: 'bar',
//type: 'pie',
categories: ["May 2012", "Jun 2012", "Jul 2012",
"Aug 2012", "Sep 2012", "Oct 2012"],
series: ["USA","Canada","Australia", "Brazil"],
colors: ['#be1e2d', '#666699', '#92d5ea', '#ee8310'],
};
myLibrary("#myCanvas",data,options);
});
这里,我们只拿几个小例子,因为我们的目的是说明性的;重要的是理解基本的方法论。例如,在前面的例子中定义画布时,您在绘图区域中指定了边距。然而,在这种情况下,让用户在库之外直接定义这些参数更合适。
在其他情况下,甚至可能有更具体的参数,典型的单一类型的图表。在这种情况下,您将有一个进一步的嵌套结构,例如一个options
对象在另一个options
对象内,该对象只特定于一种类型的图表。例如,这种方法是 jqPlot 中构成options
对象的大量属性和子属性的基础,jqPlot 是一个库,您将在本书的下一部分研究它。因此,举例来说,让我们将barGroupMargin
属性作为特定参数插入条形图(见图 7-2 )。使用此属性,您可以控制条形之间的距离。因为该属性只针对一种图表类型,所以它将在一个bar
对象中指定,该对象又包含在options
中。
图 7-2。
Setting the barGroupMargin
property, you can modify the distance between the bars
甚至画布的边距也可以定义为options
中的属性。这样,您可以调整图表的位置,而无需每次都更改myLibrary.js
库中的代码。
使用这种方法,根据options
层次中的影响区域,细分属性,将它们分配给描述该区域的对象(见图 7-3 )。
图 7-3。
The hierarchy of the options
object reflects the hierarchy of the elements that form the chart
在这种情况下,在options
对象中有许多属性需要设置,如清单 7-6 所示。
清单 7-6。ch7_01.html
$(document).ready(function(){
var data = [[12, 40, 75, 23, 42, 80],
[3, 22, 40, 27, 35, 21],
[60, 80, 16, 28, 33, 26],
[46, 7, 14, 26, 36, 24]];
var options = {
//type: 'line',
type: 'bar',
//type: 'pie',
categories: ["May 2012", "Jun 2012", "Jul 2012",
"Aug 2012", "Sep 2012", "Oct 2012"],
series: ["USA","Canada","Australia", "Brazil"],
colors: ['#be1e2d', '#666699', '#92d5ea', '#ee8310'],
margins: {top: 30, right: 10, bottom: 10, left: 30},
bar: {
barGroupMargin: 4
}
};
myLibrary("#myCanvas",data,options);
});
这样,您就完成了对网页中所有要定义的内容的定义。现在,必须使用data
数组处理输入数据并将其转换成图形元素。在您的options
对象中,您还有一系列参数来描述图表的特征。最后,您已经指出了target
,即您将在其上绘制图表的画布。所以,让我们看看myLibrary
内部来解决所有这些问题。
实现库
现在您已经完成了 web 页面中所有内容的定义,您必须开始实现您的库。如果您回过头来看看用来获得折线图、条形图和饼图的代码,您会发现这些代码有许多共同点。正是这些公共部分构成了库的主干,而那些特定于图表类型的部分将在一个if()
语句中单独实现,该语句只有在options
中选择的类型对应时才会激活。
设置画布
代码的一个公共部分是应用于画布的上下文的定义,如清单 7-7 所示。
清单 7-7。myLibrary.js
function myLibrary(target,data,options){
var canvas = $(target);
var margin = options.margins;
var w = canvas.width() - margin.left - margin.right,
h = canvas.height() - margin.top - margin.bottom;
var ctx = canvas.get(0).getContext("2d");
if(options.type === 'pie'){
ctx.strokeRect(margin.left, margin.top, w, h);
} else {
ctx.translate( 0, canvas.height() );
ctx.strokeRect(margin.left, -margin.bottom, w, -h);
}
};
如您所见,这里使用了target
参数。关于边距的定义,您必须记住在options
对象中定义它们,因此您将读取内部定义的值。获取这些值真的很简单;您只需在每次需要时定义语句:
options.
property
因此,边距将使用options.margins
。在这部分代码中,三种图表之间的唯一区别是定义设计区域的矩形,其方向与饼图的页面一致,而对于折线图和条形图,方向相反。这就是为什么只有当options.type
不同于'pie'
时才应用ctx.translate()
。
绘制轴、记号标签和网格
现在,让我们将代码添加到您的库中。这段代码处理 x 和 y 轴刻度标签的创建。只有折线图和条形图才需要这些组件,因为它们是在笛卡尔轴上表示的;饼图不使用它们。因此,您在if()
语句中应用条件,以便只为这两种类型的图表执行代码。你正在实现的代码与前几章中使用的相同,除了在这种情况下,你已经用options
对象和data
数组中可用的其他变量替换了变量(见清单 7-8)。
清单 7-8。myLibrary.js
function myLibrary(target,data,options){
...
var ctx = canvas.get(0).getContext("2d");
if(options.type === 'pie'){
ctx.strokeRect(margin.left, margin.top, w, h);
} else {
ctx.translate( 0, canvas.height() );
ctx.strokeRect(margin.left, -margin.bottom, w, -h);
}
if(options.type === 'line' || options.type === 'bar'){
var minVal = 0;
var maxVal = 0;
data.forEach(function(d,i){
var min = Math.min.apply(null, d);
if(min < minVal)
minVal = min;
var max = Math.max.apply(null, d);
if(max > maxVal)
maxVal = max;
});
maxVal = 1.1 * maxVal;
//calculate yLabels
var yLabels = [];
var yDeltaPixels = 30;
var nTicks = Math.round(h / yDeltaPixels);
var yRange = maxVal - minVal;
var yDelta = Math.ceil(yRange / nTicks);
var yVal = minVal;
while(yVal < (maxVal - yDelta)){
yLabels.push(yVal);
yVal += yDelta;
}
yLabels.push(yVal);
yLabels.push(maxVal);
//draw xLabels
if(options.type === 'line'){
var xDelta = w / (options.categories.length - 1);
}
if(options.type === 'bar'){
var xDelta = w / (options.categories.length);
}
var xlabelsUL = $('<ul class="labels-x"></ul>')
.width(w)
.height(h)
.insertBefore(canvas);
$.each(options.categories, function(i){
var thisLi = $('<li><span class="label">' + this + '</span></li>')
.prepend('<span class="line" />')
.css('left', xDelta * i)
.width(0)
.appendTo(xlabelsUL);
var label = thisLi.find('span.label');
});
//draw yLabels
var yScale = h / yRange;
var liBottom = h / (yLabels.length-1);
var ylabelsUL = $('<ul class="labels-y"></ul>')
.width(w)
.height(h)
.insertBefore(canvas);
$.each(yLabels, function(i){
var thisLi = $('<li><span>' + this + '</span></li>')
.prepend('<span class="line" />')
.css('bottom', liBottom * i)
.prependTo(ylabelsUL);
var label = thisLi.find('span:not(.line)');
var topOffset = label.height()/-2;
if(i == 0){ topOffset = -label.height(); }
else if(i== yLabels.length-1){ topOffset = 0; }
label
.css('margin-top', topOffset)
.addClass('label');
});
}
};
如果你看一下前面章节中定义的级联样式表(CSS)样式,对于三种类型的图表,你会发现它们并不完全相同,特别是对于某些类别的样式。为了克服这个问题,最简单的方法是定义这些属性,在定义它们的时候,对各种类型的样式(或者说,代表它们的标签)使用css()
函数。因此,对于所有三种类型的图表,您可以使用相同名称的样式类,但是使用不同的值,因为它们都有自己的css()
函数。
例如,当您定义管理 x 轴刻度标签的 CSS 类span.label
时,根据您是在处理折线图还是条形图,这些标签的行为必须不同。如果您想要表示折线图,将在刻度处报告刻度标签,但是如果您想要表示条形图,应该在两个刻度处报告标签。因此,您必须以不同的方式定义同一个span.label
类的属性,您可以通过添加 40 个像素的左边距和条形图独有的css()
函数来实现。代码的相关部分如清单 7-9 所示。
清单 7-9。myLibrary.js
function myLibrary(target,data,options){
...
if(options.type === 'line' || options.type === 'bar'){
...
$.each(options.categories, function(i){
var thisLi = $('<li><span class="label">' + this + '</span></li>')
.prepend('<span class="line" />')
.css('left', xDelta * i)
.width(0)
.appendTo(xlabelsUL);
var label = thisLi.find('span.label');
if(options.type === 'line'){
label.addClass('label');
}
if(options.type === 'bar'){
label.css('margin-left', '40px')
.addClass('label');
}
});
//draw yLabels
var yScale = h / yRange;
var liBottom = h / (yLabels.length-1);
...
}
};
因为我们正在讨论 CSS 类,所以让我们把你在前面章节中使用的所有定义添加到你的网页中,这些定义对所有三种类型都有效。但是,不要把它们直接添加到你的网页上,把它们写在<style></style>
标签之间,你必须把这些 CSS 定义看作是库的一部分;因此,最好将它们写在一个新的 CSS 文件中,你将称之为myLibrary.css
(见清单 7-10)。
清单 7-10。myLibrary.css
canvas {
position: relative;
}
ul,.li {
margin: 0;
padding: 0;
}
.labels-x, .labels-y {
position: absolute;
left: 37;
top: 37;
list-style: none;
}
.labels-x li {
position: absolute;
bottom: 0;
height: 100%;
}
.labels-x li span.label {
position: absolute;
color: #555;
top: 100%;
margin-top: 5px;
left:-15;
}
.labels-x li span.line{
position: absolute;
border: 0 solid #ccc;
border-left-width: 1px;
height: 100%;
}
.labels-y li {
position: absolute;
bottom: 0;
width: 100%;
}
.labels-y li span.label {
position: absolute;
color: #555;
right: 100%;
margin-right: 5px;
width: 100px;
text-align: right;
}
.labels-y li span.line {
position: absolute;
border: 0 solid #ccc;
border-top-width: 1px;
width: 100%;
}
.legend {
list-style: none;
position: absolute;
left: 520px;
top: 40px;
border: 1px solid #000;
padding: 10px;
}
.legend li span {
width: 12px;
height: 12px;
float: left;
margin: 3px;
}
.chart-title {
font-size: 24;
font-weight: bold;
position: absolute;
left: 150px;
top: 10px;
width: 100%
}
为了使这些 CSS 样式设置有效,新的 CSS 文件必须包含在你的网页中,并带有一个指向该文件的链接,如清单 7-11 所示。
清单 7-11。myLibrary.js
<HEAD>
<TITLE>MyChart</TITLE>
</HEAD>
<BODY>
<script type="text/javascript" src="../src/jquery.min.js"></script>
<link href="./myLibrary.css" rel="stylesheet" type="text/css">
<script type="text/javascript" src="./mylibrary.js"></script>
<script>
...
绘图日期
现在你必须使用画布的上下文来定义将输入数据转换成图形元素的代码部分(见清单 7-12)。这部分特定于每种类型的图表,因此每种图表都有不同的实现。
清单 7-12。myLibrary.js
function myLibrary(target,data,options){
...
if(options.type === 'line' || options.type === 'bar'){
...
}
if(options.type === 'line'){
// draw DATA
ctx.lineWidth = 5;
for(var i in data){
var points = data[i];
ctx.moveTo(0,-points[i]);
ctx.strokeStyle = options.colors[i];
ctx.beginPath();
var xVal = margin.left;
for(var j in points){
var relY = (points[j] * h / maxVal) + 10;
ctx.lineTo(xVal,-relY);
xVal += xDelta;
}
ctx.stroke();
ctx.closePath();
}
} // end of LINE
if(options.type === 'bar'){
var barGroupMargin = options.bar.barGroupMargin;
for(var i in data){
ctx.beginPath();
var n = data.length;
var lineWidth = (xDelta - barGroupMargin * 2) / n;
var strokeWidth = lineWidth - (barGroupMargin * 2);
ctx.lineWidth = strokeWidth;
var points = data[i];
var xVal = (xDelta - n * strokeWidth - (n - 1) * (lineWidth - strokeWidth)) / 2;
for(var j in points){
var relX = margin.left + (xVal - barGroupMargin) +
(i * lineWidth) + lineWidth / 2;
ctx.moveTo(relX,-margin.bottom);
var relY = margin.bottom + points[j] * h / maxVal;
ctx.lineTo(relX, -relY);
xVal += xDelta;
}
ctx.strokeStyle = options.colors[i];
ctx.stroke();
ctx.closePath();
}
} // end of bar
if(options.type === 'pie'){
var pieMargin = margin.top + 50;
var centerx = Math.round(w / 2) + margin.left;
var centery = Math.round(h / 2) + margin.top;
var radius = centery - pieMargin;
var counter = 0.0;
var dataSum = function(){
var dataSum = 0;
for(var i in data){
var points = data[i];
for(var j in points){
dataSum += points[j];
}
}
return dataSum;
}
var dataSum = dataSum();
var labels = $('<ul class="labels"></ul>')
.css('list-style','none')
.insertBefore(canvas);
for(var i in data){
var sum = 0;
var points = data[i];
for(var j in points){
sum += points[j];
}
var fraction = sum / dataSum;
ctx.beginPath();
ctx.moveTo(centerx, centery);
ctx.arc(centerx, centery, radius,
counter * Math.PI * 2 - Math.PI * 0.5,
(counter + fraction) * Math.PI * 2 - Math.PI * 0.5, false);
ctx.lineTo(centerx, centery);
ctx.closePath();
ctx.fillStyle = options.colors[i];
ctx.fill();
var sliceMiddle = (counter + fraction / 2);
var distance = radius * 1.2;
var labelx = Math.round(centerx +
Math.sin(sliceMiddle * Math.PI * 2) * (distance));
var labely = Math.round(centery -
Math.cos(sliceMiddle * Math.PI * 2) * (distance));
var leftPlus = (labelx < centerx) ? '40' : '0' ;
var percentage = parseFloat((fraction * 100).toFixed(2));
var labelval = percentage + "%";
var labeltext = $('<span class="label">' + labelval +'</span>')
.css('font-size', radius / 8)
.css('color', options.colors[i])
.css('font-weight', 'bold');
var label = $('<li class="label-pos"></li>')
.appendTo(labels)
.css({left: labelx-leftPlus, top: labely, position: 'absolute',
padding: 0})
append(labeltext);
counter+=fraction;
}
} //end of pie
};
添加图例
要在库中定义的最后一部分代码是实现图例组件的代码(见清单 7-13)。因为这一部分对于所有三种类型的图表都是相同的,所以它不受if()
语句的约束。注意,在代码中,系列由options.series
读取,颜色由options.colors
数组读取。
清单 7-13。myLibrary.js
function myLibrary(target,data,options){
...
if(options.type === 'pie'){
...
} //end of pie
//draw the legend
var legendList = $('<ul class="legend"></ul>')
.insertBefore(canvas);
for(var i in options.series){
$('<li>'+ options.series[i] +'</li>')
.prepend('<span style="background: '+ options.colors[i] +'" ></span>')
.appendTo(legendList);
}
};
如果您将三个值'pie'
、'line'
和'bar'
分配给type
属性,您将得到与前面章节中相同的三个图表,除了生成它们的代码被压缩成一个唯一的版本:库myLibrary.js
。此外,您现在拥有了能够从库外部配置一切的优势。
默认值
但是,假设您忘记了在options
中定义一个参数。接下来会发生什么?您启动的页面肯定无法正常工作。事实上,所有的库,包括非常简单的库,都必须包含在options
中定义的所有值,而这些值必须已经定义了默认值。这是一个非常重要的概念,你会在 jqPlot 和 Highcharts 库中看到它。
你已经知道所有的图形元素都可以用标准参数来描述,这些参数一个接一个地创建了一个树形结构属性,你可以在options
对象中找到。任何一个库,无论多么简单或复杂,都有这样的内部结构。实际上,每个库都必须提供它的options
结构,其中每个属性都已经指定了一个默认值,所以如果这个属性没有在options
对象中声明并作为参数传递给myLibrary()
函数,您不会得到任何错误,因为一个值已经被赋给了那个属性。然而,这样做的原因不仅仅是为了确保当您忘记输入一个值时您的库能够运行,而是为了以最小的努力获得最大的结果。想象一个比您刚刚实现的库复杂得多的库,其中要定义的属性有好几个。这种类型的库很可能是 jqPlot。正如您将看到的,您只需要定义几行代码就可以获得很好的结果。事实上,只写你想改变的参数就足够了;这为你节省了大量的时间和精力。
为了更好地理解这个概念,如果你不希望在属性barGroupMargin
中定义一个值,例如,因为它的缺省值 4 适合你的需要,那么你不需要在options
对象中写任何对它的引用,如清单 7-14 所示。
清单 7-14。ch7_01b.html
var options = {
type: 'bar',
categories: ["May 2012", "Jun 2012", "Jul 2012",
"Aug 2012", "Sep 2012", "Oct 2012"],
series: ["USA", "Canada", "Australia", "Brazil"],
colors: ['#be1e2d', '#666699', '#92d5ea', '#ee8310'],
margins: {top: 30, right: 10, bottom: 10, left: 30},
bar: {}
}
并且,这个库,经过适当的修改来处理这个值的缺失,分配了缺省值 4(见清单 7-15)。
清单 7-15。myLibrary.js
function myLibrary(target,data,options){
...
if(options.type === 'line'){
...
} // end of LINE
if(options.type === 'bar'){
if(typeof options.bar.barGroupMargin!= 'undefined') {
var barGroupMargin = options.bar.barGroupMargin;
} else {
var barGroupMargin = 4;
}
for(var i in data){
ctx.beginPath();
var n = data.length;
...
} // end of bar
...
};
图 7-4 显示了你在前三章中实现的三种类型的图表,但是这次生成它们的代码都在一个文件中。
图 7-4。
The library generates three type of charts: (a) a line chart, (b) a bar chart, and (c) a pie chart
摘要
读到这一章,你就完成了这本书的第一部分。您已经看到了如何用 JavaScript 创建一个特定于图表数据表示的库。
学习重用已经实现的代码来开发你自己的库,你已经开始理解这种类型的库是如何构造的,以及各部分执行的功能是什么。特别重要的是对树结构的介绍,我们称之为options
,许多库都有。options
对象在定义图形组件的所有设置中起着重要的作用,因此也定义了如何表示图表。
此外,您还看到了如何通过输入数组在内部管理这类库的数据,jQuery 库所扮演的角色,以及在其上构建数据的原因。
在下一章,你将开始这本书的第二部分,在这一部分中,jqPlot 和 Highcharts 库将被完整地讨论。这些库在 web 开发人员中取得了一些成功。尽管它们比你刚刚开发的库更复杂,功能更丰富,但有了它们,你会发现本章涵盖的所有概念。
八、jqPlot 简介
Abstract
从这一章开始,你将开始本书的第二部分,关于 jqPlot 库。在本章的课程中,你将会被介绍到这个库的基本概念。在了解了库的结构和组成库的文件之后,您将开始理解只使用几行代码制作图表是多么容易。
从这一章开始,你将开始本书的第二部分,关于 jqPlot 库。在本章的课程中,你将会被介绍到这个库的基本概念。在了解了库的结构和组成库的文件之后,您将开始理解只使用几行代码制作图表是多么容易。
通过一系列的例子,并通过插件的使用,你将逐渐学会如何表示任何类型的图表。一切都将使用$.jqPlot()
函数来完成,它的三个参数描述了 jqPlot 库的所有特性:目标画布、输入数据数组和 options 对象。
最后,在简要说明了如何通过使用级联样式表(CSS)样式定制图表之后,您将快速了解模块思维如何使您的实现有序、可维护和可重用。因此,让我们开始介绍这个奇妙的图书馆。
jqPlot 库
jqPlot 是一个 JavaScript 库,专门用于在网页中生成图表。jqPlot 完全用纯 JavaScript 编写,是一个开源项目,自 2009 年以来由 Chris Leonello 完全开发和维护。当扩展时,jQuery 库达到了它的全部潜在功能。正是由于这个原因,除了它的简单性,jqPlot 是当今最流行的图表表示库之一。
jqPlot 非常成功,几乎取代了其他以前的库,比如 Flot,jqPlot 保留了它的许多方面,包括外观和感觉。事实上,jqPlot 的作者经常承认他是 Flot 的忠实用户,但是随着时间的推移,他开始意识到它的局限性。旧图书馆缺乏许多功能;此外,其架构的构建方式使其难以扩展。因此,Leonello 觉得有必要创建一个新的图书馆,保留 Flot 中所有好的东西,但允许它发展。因此,他完全重写了它的架构。jqPlot 具有高度模块化的结构,正如您将看到的,它基于大量的插件,每个插件都扮演一定的角色。因此,它最大的特点是它的可插拔性。用户绘制的每一个对象,无论是线条、轴、阴影还是网格本身,都由一个插件来处理。每个绘图元素都有可定制的设置选项,每个添加的插件都可以扩展绘图的功能。
插件的数量逐渐增加,进一步扩大了库的目标。jqPlot 现在是一个多功能和可扩展的库,适合那些想在几个步骤中开发专业图表的人。
在大多数情况下,jqPlot 允许您绘制漂亮的图表,而无需添加太多行代码。事实上,您将会看到 jqPlot(也许比 jQuery 更好)已经接受了“少写多做”的理念。我认为这是图书馆最受赞赏的方面。每天都有越来越多的开发者被添加到 jqPlot 用户列表中。
包括基本档案
当您决定利用 jqPlot 在您的网站上绘制图表时,首先需要包含一组关键文件。
如前所述,jqPlot 本质上是 jQuery 的扩展,因此使用它需要包含 jQuery 插件(见表 8-1 )。您可以从 jqPlot 官方网站( www.jqplot.com
)下载这个插件,以及组成 jqPlot 库的所有其他插件,包括 CSS 文件。根据发布版本的不同,这些文件被分组到不同的发行版中。
Note
本书中的所有示例都使用 jqPlot 库的 1.0.8 版本。
表 8-1。
The distributions of jqPlot and versions of jQuery on which they are based
| jqbatch 版本 | jQuery 版本 | | --- | --- | | 1.0.6–1.0.8 | 1.9.1 | | 1.0.2–1.0.5 | 1.6.4 |但是,有一小部分文件代表了库的核心,如果您想包含 jqPlot 提供的所有功能,这些文件是必不可少的。这组基本文件由 jQuery 插件、jqPlot 插件和一个 jqPlot CSS 文件组成。还有另一个文件需要导入,但只有当您希望在低于版本 9 的 Internet Explorer 浏览器中加载页面时才需要导入:ExplorerCanvas (excanvas)脚本。这个可选文件弥补了 HTML5 引入的画布功能的不足。
因此,在您的 web 页面的<head></head>
标记中,您将包含这些文件(有关如何设置工作区的更多信息,请参见附录 A):
<!--[if lt IE 9]><script type="text/javascript" src="../src/excanvas.js"></script><![endif]-->
<script type="text/javascript" src="../src/jquery.min.js"></script>
<script type="text/javascript" src="../src/jquery.jqplot.min.js"></script>
<link rel="stylesheet" type="text/css" href="../src/jquery.jqplot.min.css" />
除了在本地使用 jqPlot 库,通过从网站下载,您还可以使用内容交付网络(CDN)服务,就像您使用 jQuery 一样。jsDelivr ( www.jsdelivr.com/#!jqplot
)是一个 CDN 网站,提供 jqPlot 的所有最新发行版。如果您想使用此服务,可以按如下方式修改 URL:
<!--[if lt IE 9]><script type="text/javascript" src="
http://cdn.jsdelivr.net/excanvas/r3/excanvas.js
]-->
>
<script src="
http://code.jquery.com/jquery-1.9.1.min.js
>
>
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/jquery.jqplot.min.js
<link rel="stylesheet" type="text/css" href="
http://cdn.jsdelivr.net/jqplot/1.0.8/jquery.jqplot.min.css
您很快就会发现,在网络中,您会遇到类似的文件,有些带有缩写“min”(表示“minified”)。您应该总是尝试使用最小版本,这些文件的压缩版本。它们加载速度更快。只有当您打算在内部修改这些库时,才应该使用它们的普通版本(不带“min”)。
绘图基础
现在您已经看到了 jqPlot 库是如何构造的,并且查看了操作它所需的文件集,您可以开始学习如何在您的 web 页面中使用这些文件。在图表开发中,有两个基本步骤:创建一个表示图表的区域,并插入一个 JavaScript 代码部分,该代码需要调用 jqPlot 库提供的所有函数、变量和一系列对象。
添加绘图容器
在第 3-7 章中,您看到了画布是如何被用作开发图表的绘图区域的。类似地,jqPlot 库需要在 HTML 页面的<body>
部分定义一个容器(一个元素)。这个容器将作为库的画布。
在本章中,该容器被称为目标。在一个网页中,每个目标(在本例中是绘图容器)由一个特定的id
标识。在本书中,您将总是发现myChart
作为目标的标识符(,但是它可以采用任何名称,并且您必须始终记住,一个以上的目标可能被分配给同一个 web 页面。此外,指定目标的宽度和高度也很重要。这些将定义网页中绘图区域的大小(见清单 8-1)。
清单 8-1。ch8_01.html
<BODY>
...
<div id="myChart" style="height:400px; width:500px;"></div>
...
</BODY>
创造情节
要输入,jqPlot 命令和几乎所有的 JavaScript 代码都必须包含在一个<scripts>
标签中。但是,一个网页分为两部分:头部和身体。那么,插入 JavaScript 代码的最佳位置是哪里呢?虽然可以将代码放在库的两个部分中,但是最好只放在一个部分中,这取决于库。考虑到 jqPlot 的工作方式,您将把代码放在<head></head>
标记之间。
此外,jqPlot 是 jQuery 的扩展,所以如果你想让你的代码被执行,你需要在$(document).ready()
函数中调用它的所有方法(见清单 8-2)。
清单 8-2。ch8_01.html
$(document).ready(function(){
// Insert all jqPlot code here.
});
然后,为了创建实际的绘图,您必须用您想要在其中绘制图表的目标的id
调用$.jqplot
插件。这个调用由下面的 jQuery 函数执行:
$.jqplot(``target, data, options
jqplot()
函数有三个参数;target,它是要在其中渲染绘图的目标元素的 ID—在绘图容器中指定的 ID 属性;数据,由数据序列数组组成;和选项,jqPlot 的主要特性。在选项中,您将输入必要的自定义设置,使您的图表更适合您的需求和口味。
如果没有定义任何选项(是的,是可选的!),您可以按照标准选项的设置生成图表。事实上,已经定义了许多选项,没有必要在每次开发图表时都更改所有设置。如果标准选项符合您的要求,则不需要定义它们。这将为您节省大量时间,并避免编写多行代码。例如,让我们编写清单 8-3 中的函数。
清单 8-3。ch8_01.html
$(document).ready(function(){
$.jqplot ('myChart', [[100, 110, 140, 130, 80, 75, 120, 130, 100]]);
});
只需几行代码,您就可以生成如图 8-1 所示的图表。
图 8-1。
A line chart created with only a few lines of code
根据您刚才看到的内容,您可以推测,如果您不指定任何选项,默认的结果将是一个折线图,并且您添加的数据将被解释为折线图。因此,数组中的值是 y 值,它们序列的索引在 x 轴上报告。在后面的章节中,你将学习如何解释这些值,以及如何从线性图表中得到不同类型的图表。
使用 jqPlot 插件
最新的 jqPlot 发行版提供了大约 30 个插件(有关所有 jqPlot 插件的列表,请参见附录 B)。每一个都专门执行一个特定的任务,其名称通常表示功能。在接下来的章节中,您将会看到许多这样的插件——它们的用途和主要选项。
让我们以 BarRenderer 为例。如果您希望将输入数据解释为条形图,则此插件是必需的:
$.jqplot ('myChart', [[100, 110, 140, 130, 80, 75, 120, 130, 100]],
{
series:[{renderer: $.jqplot.BarRenderer}]
});
在 jqPlot 中,我们经常将插件称为渲染器。这是因为框架的架构指定每个插件必须覆盖一个特定的任务。如果开发人员认为有必要,那么他或她会包含它。此外,真正的渲染器应该尽可能地相互独立。事实上,您可以添加任意数量的插件,通常它们的顺序并不重要。有些插件不需要您指定任何额外的选项或设置;它们已经被定义,并且仅仅由于被包含而被直接激活。一个这样的插件是 Highlighter,它突出显示鼠标附近的数据点。但是,如果您对默认设置不满意,您总是可以用新值定义属性;这些插件还包含其他可设置的属性。其他插件提供的功能必须在 options 参数中指定才能被激活。
因此,jqPlot 库的基本元素和附加组件(由包含的插件逐渐引入)都可以通过一系列属性来表征(与 CSS 样式非常相似)。jqPlot 库调用这些属性选项。
了解 jqPlot 选项
有效使用 jqPlot 的关键是理解 jqPlot 的选项。图表中任何对象的属性都是由属性定义的,这些属性可以取不同的值。理解如何通过我将称为选项的对象类型来设置和使用这些属性是非常重要的。
插入选项
到目前为止,您已经看到了如何在 JavaScript 代码中调用jqPlot()
函数,以及如何包含插件和数据,但是您还没有观察到如何输入选项。您可以通过向$.jqplot()
函数传递不同的属性来自定义默认折线图,如下所示:
$(document).ready(function(){
$.jqplot ('myChart', [[100, 110, 140, 130, 80, 75, 120, 130, 100]],
{
//All the attributes here.
});
});
首先要注意的是,在调用$.jqplot()
之后,不能直接在图表对象中设置属性。充其量,这不会做任何事情。您必须传递选项参数中的所有属性。
options 参数表示每个属性中的jqPlot
对象。图表的所有特征都由许多属性表示,这些属性被设置为特定的值。这些值将条形图与折线图区分开,控制线条的笔划或轴的长度,指示是否显示图例以及在哪里显示,等等。通常,当包含各种插件时,没有必要指定所有属性的值;它们已经被设置为默认值。正是因为这些缺省值,在添加插件时,您无需添加一行代码就可以实现一个漂亮的图表。如果显式指定属性,实际上是覆盖了已经用默认值定义的属性的值。
因为我们的目标是设置jqPlot
对象,并且因为它是由一系列组件组成的,所以有必要通过定义一系列对象及其属性来构建一个能够完美反映这些组件的options
对象。调用与组件相对应的对象,并为其在options
对象中的一个属性赋值,您将覆盖默认值并更改jqPlot
对象的相应组件的属性。您可以在options
对象中定义的最常用的对象有
seriesColors
stackSeries
title
axesDefaults
axes
seriesDefaults
series
legend
grid
cursor
highlighter
每个名称都反映了将受属性值更改影响的图表组件。这些对象是由一系列定义明确的属性构建的,每个属性都有自己的默认值。
这是jqPlot
对象的结构:
jqplot 对象>组件对象>对象属性>默认值
在清单 8-4 中,你可以看到在定义选项对象时需要遵循的相应结构。
清单 8-4。ch8_02c.html
var options = {
axes:{
yaxis:{
min: 70,
max: 150
},
...
},
...
};
我认为最容易添加到图表中的对象是title
。它不包含任何属性,通常被认为是jqPlot
对象的属性本身。此外,可以直接在上面设置一个文本值,该文本将成为图表的标题。鉴于其简单性,title
是理解如何使用选项的一个很好的起点(清单 8-5)。
清单 8-5。ch8_02a.html
$(document).ready(function(){
$.jqplot ('myChart', [[100, 110, 140, 130, 80, 75, 120, 130, 100]],
{
title: 'My first jqPlot chart'
});
});
如果你愿意,你也可以用jqplot()
函数从外部定义对象的属性,将属性分配给一个变量。然后这个变量将作为一个参数在jqPlot()
函数中传递,如清单 8-6 所示。这个变量实际上是options
对象。
清单 8-6。ch8_02a.html
var options = { title: 'My first jqPlot chart' };
$.jqplot ('myChart', [[100, 110, 140, 130, 80, 75, 120, 130, 100]], options);
在这两种情况下,您现在都有一个顶部带有标题的图表(见图 8-2 )。
图 8-2。
Adding a title to a line chart
为了更好地理解如何在options
对象中设置 jqPlot 属性,让我们举个例子,参考 jqPlot 网站的 API 文档部分( www.jqplot.com/docs/files/jqplot-core-js.html
)。假设您想要隐藏图表中的网格线。在属于grid
对象的属性列表中,您将找到您正在寻找的内容:
this.drawGridlines = true.
this
是网格的实例,true
是在创建jqplot
对象时分配给它的默认值。因为您希望隐藏网格(这种行为不同于默认行为),所以您需要在jqPlot
对象中用值false
替换值true
。为此,您必须在options
对象定义中添加drawGridlines
属性,维护结构对象:{property:attribute}。
options = {grid:{drawGridlines: false}};
现在,你有了一个没有网格线的图表(见图 8-3 )。
图 8-3。
Hiding the grid lines in a line chart
关于可以设置的属性的完整列表,可以去 jqPlot 官方网站( www.jqplot.com/docs/index/General.html
)或者阅读每个发行版中包含的jqPlotOptions.txt
文件。
轴上的处理选项
轴的处理与其他普通组件对象略有不同,因为它们有四个不同的子对象,即xaxis
、yaxis
、x2axis
和y2axis
。为了说明轴,我们需要一个嵌套更深的例子。假设您想要指定 y 轴上的min
和max
属性。为此,你需要用清单 8-7 所示的结构指定options
对象。
清单 8-7。ch8_02c.html
var options = {
axes:{
yaxis:{
min:70,
max:150
}
}
};
现在,y 轴上的范围在您在options
中定义的max
和min
属性之间(参见图 8-4 )。
图 8-4。
A line chart focused on a specific range on the y axis
为了使事情变得更简单,jqPlot 提供了一个方便的快捷方式,使我们能够一次为所有轴的属性分配相同的值:对象axesDefaults
。如果你想给 x 和 y(或者 x2 和 y2)设置相同的值,你只需要为axesDefaults
选项对象指定这些属性,赋值一次(见清单 8-8)。
清单 8-8。ch8_02d.html
$(document).ready(function(){
var options = {
axesDefaults:{
min:0,
max:20
}
};
$.jqplot ('myChart', [[1,4,8,13,8,7,12,10,5]], options);
});
插入一系列数据
前面,您已经看到了如何使用标准选项的设置生成简单的折线图(参见“创建图”一节)。在这个例子中,数据数组作为函数$.jqplot()
中的第二个参数被直接传递。但是,您也可以在外部将数据数组定义为变量,然后将其作为第二个参数传递。
$(document).ready(function(){
var data = [[100,110,140,130,80,75,120,130,100]];
$.jqplot ('myChart', data);
});
在这里,您可以找到与 y 轴上的值相对应的单个数据系列。但是,正如您将看到的,可以将数据作为(x,y)值对传递,也可以一次传递多个数据系列。这些输入模式各不相同,这取决于您正在设计的图表以及所使用的各种插件的要求。例如,如果你想输入多个数据序列,你需要声明四个不同的数组,如清单 8-9 所示。
清单 8-9。ch8_03a.html
$(document).ready(function(){
var series1 = [1, 2, 3, 2, 3, 4];
var series2 = [3, 4, 5, 6, 5, 7];
var series3 = [5, 6, 8, 9, 7, 9];
var series4 = [7, 8, 9, 11, 10, 11];
$.jqplot ('myChart', [series1, series2, series3, series4]);
});
jqPlot 能够管理多个系列,而无需在options
中指定任何属性。事实上,浏览器会显示一个折线图,有多少条数据系列就有多少条线,每条线都有不同的颜色,如图 8-5 所示。
图 8-5。
A multiseries line chart with different colors for each series
正如您所看到的,除了使代码更加可读和整洁之外,外部定义的数据系列还可以帮助将来扩展和操作数据。使用 JavaScript 提供的所有工具,您可以创建、操作、排序、计算和计算各种各样的数据。
甚至对于一系列数据,也可以改变options
对象的属性。这些序列按照特定的顺序被插入到一个数组中,该数组作为第二个参数在$.jqplot()
函数中传递。这个顺序将反映在jqPlot
对象中的series
对象的创建中。例如,如果您只希望第二个系列不显示其标记点,则有必要为第一个系列的属性留出空间(不覆盖其属性),然后在第二个空间中将showMarker
属性设置为'false'
。这样,jqPlot 将只覆盖第二个系列的属性值。为了实现这一点,你必须编写清单 8-10 所示的options
对象。
清单 8-10。ch8_03b.html
$(document).ready(function(){
var series1 = [1, 2, 3, 2, 3, 4];
var series2 = [3, 4, 5, 6, 5, 7];
var series3 = [5, 6, 8, 9, 7, 9];
var series4 = [7, 8, 9, 11, 10, 11];
var options = {
series: [ {},
{
showMarker: false
}]
}
$.jqplot ('myChart', [series1, series2, series3, series4], options);
});
这些设置的结果就是图 8-6 中的四个系列的图表。请注意,从顶部开始的第三个系列没有标记点。
图 8-6。
A multiseries chart with a series showing no markers
如果你决定在axesDefaults
中设置showMarker
属性,而不是在axes
对象中,你将一次为所有的序列分配相同的值(见清单 8-11)。
清单 8-11。ch8_03c.html
$(document).ready(function(){
var series1 = [1, 2, 3, 2, 3, 4];
var series2 = [3, 4, 5, 6, 5, 7];
var series3 = [5, 6, 8, 9, 7, 9];
var series4 = [7, 8, 9, 11, 10, 11];
var options = {
seriesDefaults: { showMarker: false }
};
$.jqplot ('myChart', [series1, series2, series3, series4], options);
});
现在,图表中没有一个系列显示任何标记点(见图 8-7 )。
图 8-7。
A multiseries chart with no markers
还有第三种方法将数据作为数组输入。您刚刚看到了这样一种情况,其中为每个系列定义了一个数组,然后传递给$.jqplot()
函数,所有这些都聚集在一个数组中:
var series1 = [1, 2, 3, 2, 3, 4];
var series2 = [3, 4, 5, 6, 5, 7];
var series3 = [5, 6, 8, 9, 7, 9];
var series4 = [7, 8, 9, 11, 10, 11];
$.jqplot ('myChart', [series1, series2, series3, series4], options);
但是,也可以在一个变量中定义所有的序列,我们称之为dataSets
:
var dataSets = {
data1: [[1,1], [2,2], [3,3], [4,2], [5,3], [6,4]],
data2: [[1,3], [2,4], [3,5], [4,6], [5,5], [6,7]],
data3: [[1,5], [2,6], [3,8], [4,9], [5,7], [6,9]],
data4: [[1,7], [2,8], [3,9], [4,11], [5,10], [6,11]]
};
一旦您声明了dataSets
变量,为了访问这些值,您必须指定其中的序列,并以dataSets.
作为前缀。因此,当你需要将这四个数列作为jqplot()
函数的第二个参数单独传递时,你必须这样做:
$.jqplot ('myChart', [dataSets.data1, dataSets.data2, dataSets.data3, dataSets.data4], options);
虽然,目前,这整个操作可能看起来太费力了,但是稍后您将会看到,在特殊情况下,将所有数据收集到一个数据集中是非常有用的。
渲染器和插件:进一步说明
通常,渲染器是一个对象,它被附加到绘图中的某个对象上以便进行绘制。插件除了添加绘图功能外,还可以执行其他功能,如事件处理;进行计算;以及处理字符串和值的格式,比如日期。因此,可以将渲染器视为绘图插件,但反过来就不一定了。
让我们借助一些例子来更详细地考察这种细微的差别。例如,您已经看到,通过只输入一个数据系列,您可以默认获得一个折线图(参见“创建绘图”一节)。如果要将这个系列渲染成条形图,需要将 barRenderer 插件附加到options
中的seriesDefaults
对象上。此外,当从折线图切换到条形图时,有必要在 x 轴上创建类别,以便使条形相互之间很好地分开。为此,你需要在options
中将CategoryAxisRenderer
附加到axes
对象上(见清单 8-12)。
清单 8-12。ch8_04a.html
$(document).ready(function(){
var data = [[100, 110, 140, 130, 80, 75, 120, 130, 100]];
var options = {
seriesDefaults: {
renderer: $.jqplot.BarRenderer
},
axes:{
xaxis:{
renderer: $.jqplot.CategoryAxisRenderer
}
}
}
$.jqplot ('myChart', data, options);
});
但是,调用options
中的两个渲染器是不够的。您还必须在页面中加载它们,因此您必须包含相应的插件,如清单 8-13 和 8-14 所示(CDN 服务)。
清单 8-13。ch8_04a.html
<script type="text/javascript" src="../src/jquery.min.js"></script>
<script type="text/javascript" src="../src/jquery.jqplot.min.js"></script>
<link rel="stylesheet" type="text/css" href="../src/jquery.jqplot.min.css" />
<script type="text/javascript" src="../src/plugins/jqplot.barRenderer.min.js"></script>
<script type="text/javascript" src="../src/plugins/jqplot.categoryAxisRenderer.min.js"></script>
清单 8-14。ch8_04a.html
<script src="
http://code.jquery.com/jquery-1.9.1.min.js
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/jquery.jqplot.min.js
<link rel="stylesheet" type="text/css" href="
http://cdn.jsdelivr.net/jqplot/1.0.8/jquery.jqplot.min.css
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.barRenderer.min.js
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.categoryAxisRenderer.min.js
如果在浏览器中重新加载页面,折线图就变成了条形图,如图 8-8 所示。
图 8-8。
A bar chart
通过调用这两个渲染器到options
,您可以用这些类别渲染器替换对绘图中所有系列有效的默认渲染器。后者又有几个设置为默认值的属性,这些属性也可以修改。这些属性中有许多可能是特定于该特定渲染器的,因此它们将被添加到那些已经在jqplot
对象中定义的属性中,以便为绘图引入新的功能。
即使对于这类附加属性,您也可以通过options
对象来更改默认值。首先,将所需的渲染器分配给renderer
属性。然后,在rendererOptions
属性中指定您想要设置的所有属性。所有这些属性都将在您想要操作的组件对象中指定。例如,如果你想让一个给定系列的每一条都有不同的颜色,你需要改变属性varyBarColor
,用true
替换默认值false
(见清单 8-15)。
清单 8-15。ch8_04b.html
var options = {
seriesDefaults: {
renderer: $.jqplot.BarRenderer,
rendererOptions: {
varyBarColor: true
}
},
axes:{
xaxis:{
renderer: $.jqplot.CategoryAxisRenderer
}
}
}
根据您刚才所做的更改,BarRenderer 插件将自动为每个条分配不同的颜色(参见图 8-9 )。
图 8-9。
A bar chart with different colors
插件也有特定的属性,可以在options
对象中设置。如前所述,并非所有的 jqPlot 插件都是渲染器,那些不是的插件在 jqPlot 发行版中很容易识别,因为它们的文件名中不包含术语渲染器。这些插件执行与图中特定类型的组件不直接相关的特定功能。这些特性总体上增强了 jqPlot 的功能。例如,Highlighter 是一个插件,当鼠标经过数据点时,它会高亮显示这些数据点。正如您将看到的,这个插件中有一系列工具可以处理数据值的格式说明符,并且可以用 HTML 结构显示工具提示内容。其他值得注意的插件包括 Trendline,它自动计算并绘制绘制数据的趋势线;光标,代表光标,显示在图中;和 PointLabels,它在数据点放置标签。
CSS 定制
jqPlot 图表的大部分样式是通过 CSS 完成的。jquery.jqplot.css
文件在每个发行版中都可以获得,它是为了获得 jqPlot 图表而包含在您的 web 页面中的三个基本文件之一。
组成图表的所有组件都可以通过 CSS 定制,而不必在options
对象中设置它们的任何属性。这是为了保持与网页中所有其他对象的一致性:图表的样式以及其中的所有内容(在画布内部)必须由 CSS 文件管理,就像任何其他 HTML 对象一样。控制 jqPlot 对象样式的 CSS 类的名称以前缀.jqplot-*
开始。例如,影响所有轴的样式类是.jqplot-axis
。
为了说明如何使用 CSS 修改图表的某些元素,让我们看看如何更改图表标题的字体和字体大小。与任何 HTML 元素一样,您只需调用jqPlot
元素的 CSS 选择器并修改属性。因此,在这种情况下,您在清单 8-16 中添加 CSS 样式设置。
清单 8-16。ch8_02e.html
<style>
.jqplot-title {
font:italic bold 22px arial,sans-serif;
}
</style>
有了这个新的 CSS 语句,你就改变了标题的风格,如图 8-10 所示。
图 8-10。
Two different CSS styles applied to the title
模块思维
当事情变得越来越复杂,要添加到网站的代码变得很多时,最好从模块的角度来考虑。除了提供更好的可视性和易于维护之外,创建单独的模块还可以促进您刚刚创建的内容的可重用性。让我们分析一下清单 8-17 中你的网页的现状。
清单 8-17。ch8_05a.html
<HTML>
<HEAD>
<TITLE>My first chart</TITLE>
<script type="text/javascript" src="../src/jquery.min.js"></script>
<script type="text/javascript" src="../src/jquery.jqplot.min.js"></script>
<link rel="stylesheet" type="text/css" href="../src/jquery.jqplot.min.css" />
<style>
.jqplot-title {
font: italic bold 22px arial,sans-serif;
}
</style>
<script class="code" type="text/javascript">
$(document).ready(function(){
var data = [[100, 110, 140, 130, 80, 75, 120, 130, 100]];
$(document).ready(function(){
$.jqplot ('myChart', data,
{
title: 'My first jqPlot chart'
});
});
});
</script>
</HEAD>
<BODY>
<div id="myChart" style="height:400px; width:500px;"></div>
</BODY>
</HTML>
如果您在浏览器中加载该网页,您将获得图 8-11 中的折线图。
图 8-11。
A simple line chart
显而易见,控制页面样式的部分包含在标签对
九、jqPlot 折线图
Abstract
在前一章中,您观察到了 jqPlot 的最基本用法,其中一系列数据用来绘制一条线,不需要任何附加选项。您已经看到,为了创建最基本的图表类型,即折线图,您不需要包含插件。
在前一章中,您观察到了 jqPlot 的最基本用法,其中一系列数据用来绘制一条线,不需要任何附加选项。您已经看到,为了创建最基本的图表类型,即折线图,您不需要包含插件。
在本章中,您将通过探索各种插件及其功能,开始更详细地研究 jqPlot 库提供的可能性。首先,因为折线图是在笛卡尔轴上表示的,所以将向您介绍使用成对的值(x,y)作为输入数据。然后,您将继续学习轴以及如何使用适当的插件创建它们。您还将详细分析如何将连接到轴的各种元素实现为记号、轴标签和网格。对数标度的讨论如下。
接下来,您将学习如何通过同时处理多个系列的数据来实现多系列折线图。您将发现如何通过设置线条和标记来修改图案、形状甚至颜色。此外,您将看到如何通过调整浏览器绘制图表的速度来创建动画。
此外,您将研究 jqPlot 库允许您操作不同格式的日期和时间值的方式。您还将看到如何使用 HTML 格式定制一些元素,以及突出显示数据点。在本章的最后一部分,你将处理更复杂的情况,比如生成趋势线和使用波段图。
使用(x,y)对作为输入数据
到目前为止,为了简单起见,输入数据是以一个 y 值数组的形式传入的(见清单 9-1)。如果 jqPlot 只找到 y 值,则 x 值按照它们在数组中的顺序被指定为 1、2、3 等等。
清单 9-1。ch9_01.html
$(document).ready(function(){
var plot1 = $.jqplot ('myChart', [[100,110,140,130,80,75,120,130,100]]);
});
在图 9-1 中,你可以沿着 x 轴看到一系列整数,它们是作为数据传递的数组的索引。
图 9-1。
The x axis reports the indexes of the values inserted
当您使用线性图时,最好使用具有成对数值(x,y)的数组,因为这可以避免许多复杂情况,例如需要以特定顺序输入数据,而这并不总是可能或正确的。实际上,使用成对的值,数据不应该按照 x 值递增的顺序排列;jqPlot 会帮你做到。此外,x 的值不需要等距,而是可以遵循任何分布。在清单 9-2 中,插入了成对的值(x,y ),其中 x 值既没有排序也没有均匀分布。
清单 9-2。ch9_02.html
$(document).ready(function(){
var data = [[[10,100], [80,130], [65,75], [40,130],
[60,80], [30,140], [70,120], [20,110], [95,100]]];
$.jqplot ('myChart', data);
});
在图 9-2 中,你可以看到 jqPlot 是如何对图表中的所有点进行排序的,不管它们输入的顺序如何,也不管它们是否沿 x 轴均匀分布。
图 9-2。
A simple line chart with nonuniformly distributed points on the x axis
开发折线图的第一步:轴
在详细研究折线图的更复杂的方面之前,让我们先来看看这种图表的基础:轴。如果您想开发一个实现完美数据可视化的图表,对坐标轴的正确管理是至关重要的。为此,您需要很好地理解 jqPlot 库通过使用options
对象中的特定属性提供的动作模式。
添加标题和轴标签
开发图表时,第一步是使用 CanvasAxisLabelRenderer 插件添加标题并管理轴标签。
但是,为了正常工作,这个插件需要另一个插件,一个提供编写功能的插件:CanvasTextRenderer。使用这个插件,您可以直接在画布元素上呈现标签文本。这允许您像对待任何其他图形元素一样对待文本,使您能够随心所欲地旋转文本。默认情况下,y 轴上的轴标签现在旋转了 90 度,如图 9-3 所示。
图 9-3。
Without including the CanvasAxisLabelRenderer plug-in , the y axis label is horizontal. When the plug-in is included, the y axis label is rotated vertically
要集成这一新功能,您需要将这两个插件添加到基本插件集中:
<script type="text/javascript" src="../src/plugins/jqplot.canvasTextRenderer.min.js">
</script>
<script type="text/javascript"
src="../src/plugins/jqplot.canvasAxisLabelRenderer.min.js"></script>
或者,如果您更喜欢使用内容交付网络(CDN)服务,您可以按如下方式操作:
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.canvasTextRenderer.min.js
></剧本>
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.canvasAxisLabelRenderer.min.js
></剧本>
创建了options
变量之后,你必须在里面指定一些属性,如清单 9-3 所示。您已经看到了如何通过给title
对象分配一个字符串来添加标题。然后,必须显式调用canvasAxisLabelRenderer
对象来激活它的功能,通过在axesDefaults
对象中这样做,它将对所有轴有效。要分配 x 轴和 y 轴标签中的文本,必须设置axes
对象的xaxis
和yaxis
子对象中的label
属性。它的树形结构将允许你在每个单独的轴上进行不同的改变。
清单 9-3。ch9_03a.html
$(document).ready(function(){
var data = [[100, 110, 140, 130, 80, 75, 120, 130, 100]] ;
var options = {
title: 'My Line Chart',
axesDefaults: {
labelRenderer: $.jqplot.CanvasAxisLabelRenderer
},
axes: {
xaxis: {
label: "X Axis"
},
yaxis: {
label: "Y Axis"
}
}
};
$.jqplot ('myChart', data, options);
});
图 9-4 展示了清单代码生成的图表。
图 9-4。
A line chart with the y axis label vertically oriented
轴属性
与轴标签一样,有几个属性可以在axes
对象中指定。例如,查看图表(见图 9.4),您可以看到直线从 x 值 1 开始,而 x 轴从值 0 开始,因此留下了一个空白。另一个空格出现在 x 范围的末端(9 到 10 之间)。如果您想要作用于这些距离(在轴的界限和数据集的端点之间),您必须使用pad
属性。您可以应用填充来扩展数据边界上下的范围。数据范围乘以该因子,以确定最小和最大轴边界。值 0 将被解释为没有填充,并且pad
将被设置为 1。因此,通过将pad
属性添加到xaxis
对象并将pad
设置为 1(参见清单 9-4),您得到了图 9-5 中的图表。
清单 9-4。ch9_03b.html
xaxis: {
label: "X Axis",
pad: 1
},
图 9-5。
The same line chart as in Figure 9-4, with pad
set to 1 on the x axis
现在,x 轴从值 1 开始,以 9 结束,代表数据系列的线也是如此。为了更好地理解填充的概念,你现在将设置pad
属性为 2(见清单 9-5)。这意味着您想要将当前范围(10)扩大两倍。结果,你会得到一个 x 轴从-4 到 14 的图表,如图 9-6 所示。这是因为 jqPlot 倾向于以对称的方式保存数据,将其显示在中间。
清单 9-5。ch9_03c.html
xaxis: {
label: "X Axis",
pad: 2
},
图 9-6。
The same line chart, with pad
set to 2 on the x axis
另一种控制数据显示范围的方法是使用min
和max
属性(见清单 9-6)。
清单 9-6。ch9_03d.html
xaxis: {
label: "X Axis",
min: 1,
max: 9
},
图 9-7 显示了具有新范围的 x 轴。
图 9-7。
The same line chart, with defined max
and min
on the x axis
其他有用的属性是那些控制细分(分割轴)及其底层数字术语的属性:ticks
属性。由于它们的使用不仅限于axes
对象下的简单选项——它们本身也是一个对象,并且需要一个渲染器插件才能工作——它们的处理值得用一个单独的章节来介绍。
坐标轴刻度
记号是显示图中记号或网格线值的组件。可以在options
中的axes
对象内指定图中刻度的行为,而且,作为一个对象本身,刻度有几个属性可以在tickOption
属性内设置。例如,您可能需要为每个轴设置特定数量的网格线。这可以用不同的方法来完成。最简单的是直接指定numberTicks
属性(见清单 9-7)。如果将其值设置为 5,您将在 x 轴上得到五个刻度:0、3、6、9 和 12(参见图 9-8 )。
清单 9-7。ch9_03e.html
xaxis: {
label: "X Axis",
numberTicks: 5
},
图 9-8。
A line chart with a prefixed number of ticks on the x axis
这也适用于 y 轴。在这种情况下,您需要在yaxis
对象中设置相同的属性。从图中可以看出,x 轴的间隔是均匀的,因此刻度是等距的。另一种方法是直接定义你想要在图表上显示的刻度,如清单 9-8 所示。
清单 9-8。ch9_03f.html
xaxis: {
label: "X Axis",
ticks: [0,3,6,9,12]
},
这产生了相同的图表(见图 9-9 )。
图 9-9。
A line chart with directly defined ticks on the x axis
但是,当您希望刻度沿轴不均匀分布时,通常最好使用这种方法,如清单 9-9 所示。网格线也将遵循这种不均匀性,因为它将与每个刻度对应绘制(见图 9-10 )。
清单 9-9。ch9_03g.html
xaxis: {
label: "X Axis",
ticks: [1,2,3,7,9]
},
图 9-10。
A line chart with nonuniform, prefixed ticks on the x axis
刻度在图表中如此重要,以至于它们有一个专门针对它们的插件:CanvasAxisTickRenderer。
如果希望创建一个没有网格线的图表,同时保持刻度上的值,可以将showGridLine
属性设置为'false'
。但是,在此之前,您需要在 web 页面中包含插件:
<link rel="stylesheet" type="text/css" href="../src/jquery.jqplot.min.css" />
<script type="text/javascript"
src="../src/plugins/jqplot.canvasTextRenderer.min.js"></script>
<script type="text/javascript"
src="../src/plugins/jqplot.canvasAxisLabelRenderer.min.js"></script>
<script type="text/javascript"
src="../src/plugins/jqplot.canvasAxisTickRenderer.min.js"></script>
或者,如果您更喜欢使用 CDN 服务,您可以按如下方式操作:
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins
/jqplot.canvasTextRenderer.min.js"></script>
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins
/jqplot.canvasAxisLabelRenderer.min.js"></script>
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins
/jqplot.canvasAxisTickRenderer.min.js"></script>
然后你必须在axesDefaults
对象内进行设置,因为你想隐藏两个轴的网格线。记得调用刚刚包含在tickRenderer
属性中的插件(见清单 9-10)。此外,您一定不要忘记删除在xaxis
对象中定义的ticks
属性。
清单 9-10。ch9_04a.html
axesDefaults: {
labelRenderer: $.jqplot.CanvasAxisLabelRenderer,
tickRenderer: $.jqplot.AxisTickRenderer,
tickOptions: {
showGridline: false
}
},
axes: {
xaxis: {
label: "X Axis" //remove the comma here
},
如图 9-11 所示,你得到一个没有网格的图表。
图 9-11。
A line chart without grid lines
有时,你只需要隐藏一个轴的网格线,例如 x 轴(见图 9-12 )。在这种情况下,您必须只在xaxis
对象内部调用渲染器。在清单 9-11 中,你可以看到必须从axesDefaults
中删除然后写入xaxis
对象的代码行。
清单 9-11。ch9_04b.html
axesDefaults: {
labelRenderer: $.jqplot.CanvasAxisLabelRenderer
//delete all this lines
//tickRenderer: $.jqplot.AxisTickRenderer,
//tickOptions: {
//showGridline: false
//}
},
axes: {
xaxis: {
label: "X Axis",
tickRenderer: $.jqplot.AxisTickRenderer,
tickOptions: {
showGridline: false
}
},
...
图 9-12。
A line chart with only horizontal grid lines
您可能希望添加的另一个功能是允许您将数值的格式作为字符串来处理。最常见的情况是,当您想要在 y 轴上显示百分比值时,这可能是有用的。要做到这一点,你需要在数值后面加上字符'%'
,如清单 9-12 所示。
清单 9-12。ch9_04c.html
yaxis: {
label: "Y Axis",
tickRenderer: $.jqplot.AxisTickRenderer,
tickOptions: {
formatString:'%d%'
}
}
如图 9-13 所示,图表现在在 y 轴上报告百分比值。
图 9-13。
A line chart reporting percentages on the y axis
稍后,您将看到这种字符串格式被证明是一种非常强大的工具的其他情况(参见“处理日期值”一节)。
使用对数标度
根据要在图表中表示的数据趋势,有时有必要在一个或两个轴上使用对数刻度。jqPlot 支持对数标度,包括 web 页面中的 LogAxisRenderer 插件。
<script type="text/javascript"
src="../src/plugins/jqplot.logAxisRenderer.min.js"></script>
LOG SCALE
对数标度使用与数量级(通常为 10)相对应的间隔,而不是标准的线性标度。这允许您在轴上表示大范围的值(v)。
对数是指数的另一种写法,你可以用它来分隔指数(x)并把它放在一个轴上。
例如,对数标度上一个点的增加对应于该值的 10 倍增加。同样,增加 2 个点相当于该值增加 100 倍。等等。
在想要以对数刻度表示数据的轴上,只需添加带有插件引用的renderer
属性。在这种情况下,您需要创建一个近似遵循指数趋势的数据数组。所以,你在清单 9-13 中使用了[x,y]对的数组。
清单 9-13。ch9_11.html
var data = [[0,1.2],[10,2.4],[20,5.6],[30,12],[40,23],
[50,60],[60,120],[70,270],[80,800]];
接下来,你把 y 轴放在对数刻度上,如清单 9-14 所示。
清单 9-14。ch9_11.html
$.jqplot ('myChart', [data],{
axes:{
xaxis:{},
yaxis:{ renderer: $.jqplot.LogAxisRenderer }
}
});
在图 9-14 中,您可以看到数据是如何在半对数标度(一个轴上的对数标度)中呈现出近似于直线的形状。
图 9-14。
A line chart on a semilog scale on the y axis
多系列折线图
既然已经很好地指定了表示折线图的坐标轴,现在是处理多系列折线图的时候了。通常,您需要在同一个图表中显示多个数据系列。事实上,图表的目的通常就是比较不同的数据序列。
jqPlot 库为我们提供了管理多系列图表所需的工具。通过对线条和标记的图案、形状和颜色进行操作,可以引入有助于表示不同数据系列的图形效果。
多系列数据
到目前为止,您只处理了一组数据。但是,有时您希望一次表示多个数据集。在第一章中,你看到了在 jqPlot 中,多个系列的处理方式与单个系列相同。每个系列必须首先通过将它赋给一个变量来单独定义,然后在一个数组中与其他系列组合。然后这个数组作为第二个参数传递给jqPlot()
函数(见清单 9-15)。
清单 9-15。ch9_05a.html
$(document).ready(function(){
var data1 = [1, 2, 3, 2, 3, 4];
var data2 = [3, 4, 5, 6, 5, 7];
var data3 = [5, 6, 8, 8, 7, 9];
var data4 = [7, 8, 9, 9, 10, 11];
var options = {
title:'Multiple Data Arrays'
};
$.jqplot ('myChart', [data1, data2, data3, data4], options);
});
图 9-15 显示了清单 9-15 中的多序列图表。
图 9-15。
A multiseries line chart
系统自动为每个系列赋予不同的颜色。这个颜色序列在 jqPlot 中被定义为缺省值。以下是 jqPlot 将按顺序分配给系列的颜色:
seriesColors: [ "#4bb2c5", "#c5b47f", "#EAA228", "#579575",
"#839557", "#958c12", "#953579", "#4b5de4",
"#d8b83f", "#ff5800", "#0085cc"]
这些值代表'#rrggbb'
,其中 rr、gg 和 bb 是红色、绿色和蓝色的十六进制值。浏览器结合这些值来生成系列所需的所有颜色。
当有超过 11 个系列时,jqPlot 将从头开始再次启动该系列。如果您不希望这样,或者只是需要做些不同的事情,您可以在seriesColors
属性中定义一个不同颜色序列的数组,如清单 9-16 中给出的序列。图 9-16 显示了一种灰色的变化,但是运行这个例子,自己看看有什么不同(颜色从蓝色到紫色)。
Note
要检查颜色代码,我建议访问网站 HTML 颜色代码( http://html-color-codes.info
)。
清单 9-16。ch9_05b.html
var options = {
seriesColors: ["#105567","#805567","#bb5567","#ff5567"],
title:'Multiple Data Arrays'
};
图 9-16。
A multiseries line chart with a customized color set
你也可以属性一个特定的颜色,使用两个函数:rgba(
r ,
g ,
b ,
a )
和rgb(
r ,
g ,
b )
。将这些函数直接插入到分配给seriesColors
属性的数组的每个值中,如清单 9-17 所示。
清单 9-17。ch9_05c.html
seriesColors: ["rgba(16,85,103,0.2)", "rgba(128,85,103,0.6)",
"rgb(187,85,103)", "rgb(250,85,103)"],
鉴于您已经通过获得给定颜色所需的红光、绿光和蓝光的组合来指定颜色,使用rgba()
函数,引入了一个新变量a
。这个a
(代表“alpha”)代表一种颜色的不透明度。如图 9-17 所示,定义低 alpha 值可以让你看到有色物体背后的东西。
图 9-17。
A multiseries line chart with different levels of transparency
平滑线图
除了选择是否表示点标记和连接它们的直线,通常你会决定你想要得到一个平滑的曲线进展,如图 9-18 所示。这可以简单地通过使用smooth
属性并将其设置为'true'
来完成(见清单 9-18)。
清单 9-18。ch9_06.html
$(document).ready(function(){
var data1 = [1, 2, 3, 2, 3, 4];
var data2 = [3, 4, 5, 6, 5, 7];
var data3 = [5, 6, 8, 8, 7, 9];
var data4 = [7, 8, 9, 9, 10, 11];
var options = {
title:'Multiple Data Arrays',
seriesDefaults: {
rendererOptions: {
smooth: true
}
}
};
$.jqplot ('myChart', [data1, data2, data3, data4], options);
});
图 9-18。
A multiseries line chart with smoothed lines
线条和标记样式
设计折线图时,您需要考虑的另一个关键方面是线条和标记的显示方式。您可以使用线条、标记序列或两者来表示图表。默认情况下,jqPlot 显示每个系列,每个点对应于[x,y]对的点标记和一条按顺序连接它们的线。
所有这些都可以使用 series 对象的两个关键属性来控制:linePattern 和 lineWidth 在添加markerOptions
属性的同时,还可以作用于影响标记组件的另外两个属性:style 和 size。清单 9-19 是这些设置的一个例子。
清单 9-19。ch9_07a.html
$(document).ready(function(){
var data1 = [1, 2, 3, 2, 3, 4];
var data2 = [3, 4, 5, 6, 5, 7];
var data3 = [5, 6, 8, 9, 7, 9];
var data4 = [7, 8, 9, 11, 10, 11];
var options = {
title: 'Multiple Data Arrays',
series:[{
linePattern: 'dashed',
lineWidth:2,
markerOptions: { style: 'diamond' }
},
{
showLine:false,
markerOptions: { size: 7, style: 'x' }
},
{
markerOptions: { style: 'circle' }
},
{
lineWidth:5,
linePattern: 'dotted',
markerOptions: { style: 'filledSquare', size: 10 }
}]
}
$.jqplot ('myChart', [data1, data2, data3, data4], options);
});
图 9-19 展示了清单 9-19 中设置的结果。
图 9-19。
In a line chart it is possible to set different markers and patterns
图表上的线条可以用linePattern
属性绘制为实线、虚线或点线。默认情况下,绘制的每条线都是实心的,所以如果您想要一条线有不同的样式,有必要在options
中指定。你可以在清单 9-19 中看到,可以将linePattern
属性设置为'dotted'
或'dashed'
,以便分别获得虚线或虚线。在清单 9-20 中,你可以看到也可以获得一个定制的线型,将格式定义为一个数组([破折号长度,间隙长度等等])。当分配给linePattern
属性的数组有偶数个元素时,线条看起来最好,这样线条以破折号开始,以空格结束。linePattern
属性也可以创建一个定制的模式,使用破折号(-
)和点(.
)字符的简写字符串符号。清单 9-20 提供了一些例子。
清单 9-20。ch9_07b.html
var options = {
title: 'Multiple Data Arrays',
seriesDefaults: {
showMarker: false
},
series: [{ linePattern: 'dashed'},
{ linePattern: 'dotted'},
{ linePattern: [4, 3, 1, 3, 1, 3]},
{ linePattern: '-.'}]
};
图 9-20 显示了清单 9-20 中使用的定制线条模式的例子。
图 9-20。
A multiseries line chart with different patterns
动画图表
当您在浏览器中加载网页时,您会注意到图表几乎是即时绘制的。你可以放慢绘图速度,根据你的喜好进行调整;当绘制元素时,较慢的速度会给图表带来浮动效果(见清单 9-21)。
清单 9-21。ch9_23.html
var options = {
title: 'Multiple Data Arrays',
seriesDefaults: {
showMarker: false,
rendererOptions: {
smooth: true,
animation: { show: true }
}
}
};
图 9-21 显示了绘制图表的顺序,给人一种动画的感觉。
图 9-21。
An animated multiseries line chart
多个 y 轴
jqPlot 支持与同一个 x 轴相关的多个 y 轴。当您希望在单个图表中显示分布在不同 y 刻度上但具有相同 x 值的不同系列时,这很有用。在这种情况下,明智的做法是用相应系列的颜色来设置 y 轴,以便您可以确定任何给定点的正确 y 值。作为输入数据,让我们创建三个数据数组,它们包含相同的 x 值,但 y 值分布在不同的范围内,如清单 9-22 所示。使用相同的 x 值不是强制性的,但这样做是明智的。
清单 9-22。ch9_12.html
var data1 = [[10, 200], [20, 230], [30, 214], [40, 212], [50, 225], [60, 234]];
var data2 = [[10, 455], [20, 470], [30, 465], [40, 432], [50, 455], [60, 464]];
var data3 = [[10, 40], [20, 60], [30, 54], [40, 52], [50, 65], [60, 54]];
为每个 y 轴指定正确的数值范围是非常重要的,以便能够容易地比较不同系列的数值(见清单 9-23)。在series
对象中,您需要明确指定三个值,每个值将一个序列分配给不同的 y 轴。如果您想保留特定系列的默认设置,即沿默认 y 轴的表示,您仍必须在对应于该系列的位置分配一个空对象{}
。事实上,在这个例子中,series
数组的第一个元素只是一个空对象{}.
此外,您需要将axesDefaults
对象的useSeriesColor
属性设置为'true'
。这样,jqPlot 会将系列的颜色分配给相应的 y 轴。因此,通过使用三种默认颜色,第一个系列将为浅蓝色,第二个系列为橙色,第三个系列为灰棕色。
清单 9-23。ch9_12.html
var options = {
series:[
{},
{yaxis: 'y2axis'},
{yaxis: 'y3axis'}
],
axesDefaults:{useSeriesColor: true},
axes:{
xaxis: {min: 0, max: 70},
yaxis: {min: 190, max: 240},
y2axis: {min: 430, max: 480},
y3axis: {min: 35, max: 80}
}
};
$.jqplot ('myChart', [data1, data2, data3], options);
图 9-22 显示了三个系列,每个系列都以其 y 轴值表示。这里的轴以不同的灰度阴影显示,但它们实际上采用了与相关系列相对应的颜色。
图 9-22。
A multiseries line chart with multiple y axes
JavaScript 数据
如前所述,最好在jqPlot
函数之外单独定义数据数组。您已经看到了如何创建一个包含 y 值或[x,y]对数值的数组。然而,因为 jqPlot 属于 JavaScript 领域,所以有另一种方法经常被证明是非常有用的:通过 JavaScript 方法生成数据序列。
使用数学函数生成数据
jqPlot 库是基于 JavaScript 的,并且和所有编程语言一样,它允许您实现生成值序列以用作输入数据的函数。例如,清单 9-24 采用了三个最常用和最著名的数学函数(正弦、余弦、幂)并通过它们创建了一个数据数组。
清单 9-24。ch9_08a.html
$(document).ready(function(){
var options = {
title:'Math function Arrays'
};
varcosPoints = [];
for (vari=0; i< 2 * Math.PI; i += 0.1){
cosPoints.push([i, Math.cos(i)]);
}
varsinPoints = [];
for (vari=0; i< 2 * Math.PI; i += 0.1){
sinPoints.push([i, 2 * Math.sin(i-.8)]);
}
varpowPoints = [];
for (vari=0; i< 2 * Math.PI; i += 0.1) {
powPoints.push([i, 2.5 + Math.pow(i/4, 2)]);
}
$.jqplot ('myChart', [cosPoints, sinPoints, powPoints], options);
});
图 9-23 说明了列表中三个函数生成的点如何在折线图上形成三个数学函数的特征趋势。
图 9-23。
A line chart reporting three different series of data generated from mathematical functions
因为这是一个具有高密度点的函数,并且因为这里的目标是突出趋势,所以最好不要显示标记点(见图 9-24 )。最好在options,
中启用平滑,如清单 9-25 所示。
清单 9-25。ch9_08b.html
var options = {
title: 'Math function Arrays',
seriesDefaults: {
rendererOptions: {
smooth: true
},
markerOptions: { show: false }
}
};
图 9-24。
The same line chart, but rendered more legibly
生成随机数据
您已经看到了如何使用数学函数生成输入数据。同样,有时需要生成随机数据。例如,假设您刚刚完成 jqPlot 图表的编写,并且想要尝试输入虚拟数据。为此,使用随机生成的数据是最好的。清单 9-26 中的函数产生随机数据,每个点都是根据前一个点的值产生的。在每一步中,新值由一个随机数决定,该随机数与前一个数相加或相减。这会产生一系列连续的数据,从作为参数传递给函数的值开始。
清单 9-26。ch9_09.html
function generateRandomData(npts, start, delta) {
var data = [];
if (delta == null) {
delta = start;
start = (Math.random() - 0.5) * 2 * delta;
}
for (j=0; j<npts; j++) {
data.push([j, start]);
start += (Math.random() - 0.5) * 2 * delta;
}
return data;
}
您使用了三个参数:npts
是要生成的点数,start
是初始值,delta
是每步随机加减的最大值。该函数返回一个数组,该数组将作为输入数据传递给图表。您可以从外部定义它:
var data = generateRandomData(30, 100, 1);
$.jqplot('myChart', [data]);
或者,你可以直接传递:
$.jqplot ('myChart', [makeContinuousData(30, 100, 1)]);
结果你得到了一个类似图 9-25 的图表(每次都会不一样)。
图 9-25。
A line chart representing a random series of data
处理日期值
日期类型是一种常用的值,尤其是在其他图表(如条形图)中。这些专门化的值不是那么容易处理的,jqPlot 有一个针对它们的插件:DateAxisRenderer。这个插件扩展了 JavaScript 的本机日期处理功能,允许您以任何明确的形式表示日期值,而不仅仅是以毫秒为单位。
DateAxisRenderer 插件
日期可以用多种方式表示,其格式因国家和用途而异。日期由日、月和年指示器组成。这些可以不同地排序,并且具有一位、两位或四位数字;或者,您甚至可能希望只使用一个或两个指示器(例如,月、年)。此外,各种字符充当分隔符。让我们以 04/07/2012 为例:“4”代表第四个月(四月),“7”是该月的第七天,“2012”是年份。这样的日期可以用多种方式显示:'07/04/2012'
、'07/04/12'
、'04/07/12'
、'7-Apr-12'
、'7-Apr'
、'Apr-12'
、'7 April'
、'2012'
等等。
日期值的标准格式如下:
'YYYY-MM-DD HH:MM<PM or AM>'
这个字符串包含了所有必要的信息——可能有点多。事实上,您通常只需要日期信息的一部分:有时,您可能只需要报告日和月,或者,如果您引用时间,您可能只需要处理小时和分钟,等等。
一旦包含了 DateAxisRenderer 插件,jqPlot 几乎可以接受任何可识别的值。在值被内部解析后,它将被呈现在调用插件的轴上,以tickOptions.formatString
中指定的格式表示。
表 9-1 显示了可接受的格式代码。
表 9-1。
Date and Time Formats Accepted by jqPlot
| 密码 | 结果 | 描述 | | --- | --- | --- | | 年 | | | | `%Y` | Two thousand and eight | 四位数年份 | | `%y` | 08 | 两位数的年份 | | 月份 | | | | `%m` | 09 | 两位数的月份 | | `%#m` | nine | 一位数或两位数的月份 | | `%B` | 九月 | 完整的月份名称 | | `%b` | 九月 | 缩写月份名 | | 天 | | | | `%d` | 05 | 两位数的月份日期 | | `%#d` | five | 一位数或两位数的月份中的某一天 | | `%e` | five | 一位数或两位数的月份中的某一天 | | `%A` | 在星期日 | 星期几的全名 | | `%a` | 太阳 | 星期几的缩写名称 | | `%w` | Zero | 一周中的第几天(0 =星期日,6 =星期六) | | `%o` | 泰国(Thailand) | 一个月中某一天之后的序数后缀字符串 | | 小时 | | | | `%H` | Twenty-three | 24 小时制的小时数(两位数) | | `%#H` | three | 24 小时整数格式的小时数(一位数或两位数) | | `%I` | Eleven | 12 小时制的小时数(两位数) | | `%#I` | three | 12 小时整数格式的小时数(一位数或两位数) | | `%p` | 下午 | 上午或下午 | | 分钟 | | | | %M | 09 | 分钟(两位数) | | %#M | nine | 分钟(一位数或两位数) | | 秒 | | | | `%S` | 02 | 秒(两位数) | | `%#S` | Two | 秒(一位数或两位数) | | `%s` | 1206567625723 | Unix 时间戳(1970 年 1 月 1 日 00:00:00 之后的秒数) | | 毫秒 | | | | `%N` | 008 | 毫秒(三位数) | | `%#N` | eight | 毫秒(一到三位数) | | 时区 | | | | `%O` | Three hundred and sixty | 当地时间和格林威治标准时间(GMT)之间的时差(分钟) | | `%Z` | 山地标准时间(MST) | 浏览器报告的时区名称 | | `%G` | –06:00 | GMT 之间的小时和分钟 | | 快捷指令 | | | | `%F` | 2008-03-26 | %Y-%m-%d | | `%T` | 05:06:30 | %H:%M:%S | | `%X` | 05:06:30 | %H:%M:%S | | `%x` | 03/26/08 | %m/%d/%y | | `%D` | 03/26/08 | %m/%d/%y | | `%#c` | 2008 年 3 月 26 日星期三下午 3:31 | %a %b %e %H:%M:%S %Y | | `%v` | 2008 年 9 月 3 日 | %e-%b-%Y | | `%R` | twenty nine to four p.m. | %H:%M | | `%r` | 下午 3 时 31 分 | %I:%M:%S %p | | 特性 | | | | `%n` | \n | 换行 | | `%t` | \t | 标签 | | `%%` | % | 百分比符号 |为了更清楚地了解 jqPlot 如何处理日期值,让我们看一系列说明各种格式的例子。然而,不管是哪种格式,您都必须在 web 页面的<head>
部分包含 DateAxisRenderer 插件。
<script type="text/javascript"
src="../src/plugins/jqplot.dateAxisRenderer.min.js"></script>
处理不同格式的日期值
第一个例子处理一段时间内的汇率,每天的点值。为此,输入数据数组内部应该有一系列[x,y]值,其中 x 是一个日期值。x 值的序列不符合时间顺序;jqPlot 将沿着 x 轴对这些点进行排序。在清单 9-27 中,你使用了一系列的 x 输入值,前五个值有不同的格式。
清单 9-27。ch9_13a.html
var line1 = [['14-Oct-2012', 1300.41], ['2012-10-15', 1310.50],
['2012/10/16', 1322.88], ['17 Oct 2012', 1312.41],
['10/18/2012', 1308.16], ['19-Oct-2012', 1310.71],
['20-Oct-2012', 1305.01],['21-Oct-2012', 1300.85],
['22-Oct-2012', 1290.67]];
接下来,您必须调用options
中的xaxis
对象内部的渲染器来激活它。您希望表示一个月中跟随汇率值趋势的日子,因此您将设置不包括年份的输出格式,年份保持不变。此外,在开始时,您希望以数字形式显示一个月中的某一天,然后用前三个字符书写月份,中间用空格隔开。简单地说,在清单 9-28 中,格式将是'%d %b'
,其中%d
代表数字形式的日,%b,
代表月份的前三个字符。y 值是美元,所以需要添加美元符号($)
作为 y 轴刻度的前缀。为此,您还必须对 y 刻度使用formatString
属性。
清单 9-28。ch9_13a.html
var options = {
title: 'Handling Date Values',
axes:{
xaxis:{
renderer: $.jqplot.DateAxisRenderer,
tickOptions:{
formatString:'%d %b'
}
},
yaxis:{
tickOptions:{
formatString:'$%d'
}
}
}
};
$.jqplot('myChart', [line1], options);
图 9-26 显示美元值,前缀$,
在 y 轴上,日和月在 x 轴上。这只是您可以设置来表示刻度值的几种格式之一。
图 9-26。
A line chart with date values on the x axis
处理时间值
假设您想要绘制一个图表来表示参观博物馆的次数。可以在输入数据中明确显示时间(小时、分钟、秒)。这允许你以与前一个例子(见清单 9-27)相同的方式处理这些时间值,例如,通过创建一个包含某一天收集的数据的图表。这里也可以用前面讨论过的任何格式设置日期。您可以用各种方式表示时间:12 小时格式,带 am 或 pm 后缀,或者直接用 24 小时格式,包括或忽略秒和分钟。清单 9-29 展示了一个以 2 小时为间隔的时间值序列。
清单 9-29。ch9_13b.html
var line1 = [['2012-10-14 08:00AM', 30],['2012-10-14 10:00AM', 60],
['2012-10-14 00:00PM', 120], ['2012-10-14 02:00PM', 60],
['2012-10-14 04:00PM', 100], ['2012-10-14 06:00PM', 40]];
关于输出格式,您必须记住还要管理时间格式;因为您只对一天中的几个小时感兴趣,所以您将'%R'
设置为'formatString'
(见清单 9-30)。
清单 9-30。ch9_13b.html
var options = {
title: 'Museum Visitors',
axes:{
xaxis:{
label: 'time',
renderer:$.jqplot.DateAxisRenderer,
tickOptions:{
formatString: '%R'
}
},
yaxis:{
label: 'visitors'
}
}
};
$.jqplot('myChart', [line1], options);
浏览器将显示如图 9-27 所示的图表。
图 9-27。
A bar chart with time values on the x axis
突出
可以添加到图表中的一个引人注目的效果是突出显示(即,让您的绘图对鼠标悬停做出反应。例如,荧光笔插件会高亮鼠标附近的数据点,具有很好的动态效果。这可以通过显示带有数据点值的工具提示来增强。
光标荧光笔
下面的例子将帮助你熟悉高亮显示。该功能非常重要,当鼠标悬停在图表中的特定元素上时会激活一个事件。通常,这些是表示数据的元素,例如,在折线图中,用一个点(或者,更准确地说,用标记;您会发现这也适用于其他类型的图表:条形图中的条形、饼图中的切片等等。
默认情况下,触发的事件只是数据的一个突出显示,由显示其(x,y)值的工具提示表示。
要将这些功能添加到图表中,您必须包括一组插件:
<script type="text/javascript" src="../src/plugins/jqplot.highlighter.min.js">
</script>
<script type="text/javascript" src="../src/plugins/jqplot.cursor.min.js">
</script>
在清单 9-31 中,作为输入数据,你使用了一系列的[x,y]对,其中日期值在 x 轴上,数值在 y 轴上。
清单 9-31。ch9_14a.html
var line1 = [['14-Oct-12', 1300.41], ['15-Oct-12', 1310.50],['16-Oct-12', 1322.88],
['17-Oct-12', 1312.41],['18-Oct-12', 1308.16],['19-Oct-12', 1310.71],
['20-Oct-12', 1305.01],['21-Oct-12', 1300.85],['22-Oct-12', 1290.67]];
正如您已经看到的,为了处理日期值,您需要包括 DateAxisRenderer 插件。
<script type="text/javascript"
src="../src/plugins/jqplot.dateAxisRenderer.min.js"></script>
在清单 9-32 中,你可以看到options
对象,包含两个新对象:highlighter
和cursor
。
清单 9-32。ch9_14a.html
var options = {
title: 'Data Point Highlighting',
axes:{
xaxis:{
renderer: $.jqplot.DateAxisRenderer,
tickOptions:{
formatString: '%b %#d'
}
},
yaxis:{
tickOptions:{
formatString: '$%d'
}
}
},
highlighter:{
show: true,
sizeAdjust: 7.5
},
cursor:{
show: false
}
};
在图 9-28 中,当光标移动到图表上的数据点上时,会出现一个工具提示。默认情况下,此工具提示使用轴格式化程序报告 x 和 y 值,用逗号分隔,但这可以用不同的格式字符串自定义。
图 9-28。
Data point highlighting on a line chart
在清单 9-32 中,你会注意到光标已经被禁用,通过将它的show
属性设置为'false'
(默认情况下是启用的)。启用它,如清单 9-33 所示,你会看到鼠标光标在进入图形区域时发生变化,并在右下角显示一个可选的工具提示,报告鼠标位置。工具提示可以位于固定位置,也可以跟随鼠标移动。默认设置为'crosshair'
的指针样式也可以自定义。
清单 9-33。ch9_14b.html
...
highlighter: {
show: true,
sizeAdjust
: 7.5
}
,
cursor: {
show: true,
tooltipLocation:'ne'
}
});
图 9-29 显示了报告光标坐标的工具提示。请注意,光标由图表中间的黑色十字表示。
图 9-29。
A line chart showing the cursor coordinates
用 HTML 格式突出显示
您可以使用 HTML 标签作为格式来更改工具提示的内容。这使得定制的可能性几乎是无限的。事实上,你可以把工具提示想象成一个小小的网页,可以在其中添加任何类型的元素,比如一张图片或者一个锚链接(更多细节,参见第一章 0)。例如,您可以使用清单 9-34 所示的设置,将 HTML 格式字符串分配给formatString
属性。
清单 9-34。ch9_14c.html
highlighter: {
show: true,
sizeAdjust: 7.5,
showMarker: false,
tooltipAxes: 'xy',
yvalues: 4,
formatString:'<table class="jqplot-highlighter"> \
<tr><td>date:</td><td>%s</td></tr> \
<tr><td>value:</td><td>%s</td></tr></table>'
},
因此,带有内容的工具提示将表现得像一个小的 HTML 页面,如图 9-30 所示。
图 9-30。
A line chart with an HTML tool tip
与图表交互:限制线条和缩放
一旦你有了一个包含图形和元素的折线图,下一步就是引入交互元素。例如,用户可能需要使用阈值来查看这些值之外的数据。用户可能还需要改变该阈值,以确定哪些数据在阈值之内,哪些数据在阈值之外。通常,会表示大量数据。在这种情况下,用户可能只需要分析一个细节。
jqPlot 库为限制线和缩放这两种情况提供了解决方案。让我们来看一些详细解决这些问题的例子。
在图表上画一条界限线
另一个非常有用的特性是 CanvasOverlay 插件。它能让你在图表上画水平线和垂直线,目的是指示一个极限、一个阈值、一个截止日期或划定一个特定的范围。这可以通过在网页中包含 CanvasOverlay 插件来实现:
<script type="text/javascript"
src="../src/plugins/jqplot.canvasOverlay.min.js"></script>
通过包含这个插件,您在options
: canvasOverlay
中有了一个新对象。在这个对象中,您将使用对象的属性定义一个对象数组。这些对象中的每一个都将由绘制在画布上的一条线来表示,jqPlot 在画布上创建您的图表。在canvasOverlay
中已经定义了五种类型的对象:
horizontalLine
verticalLine
dashedHorizontalLine
dashedVerticalLine
Line (generic)
要查看如何在图表中插入这些限制线,让我们从一个简单的折线图开始,在该折线图中,您希望显示两条不同颜色的水平限制线:一条红线标记上限,一条蓝色虚线标记下限。
在清单 9-35 中,你定义了两个对象:一个是下限的horizontalLine
,一个是上限的dashedHorizontalLine
。一旦定义了这两条线,就必须指定它们的属性。它们的属性,比如 y 值、lineWidth
和color
,其含义是显而易见的。lineCap
属性指定放置在线条上的结束类型;可以是round
、butt
或square
。
清单 9-35。ch9_15.html
$(document).ready(function(){
var data = [100, 110, 140, 130, 80, 75, 120, 130, 100];
var options = {
canvasOverlay: {
show: true,
objects: [
{horizontalLine: {
y: 70,
lineWidth: 3,
color: 'rgb(255, 0, 0)',
shadow: true,
lineCap: 'butt'
}},
{dashedHorizontalLine: {
y: 145,
lineWidth: 4,
color: 'rgb(0, 0, 255)',
shadow: false,
dashPattern: [8, 16],
lineCap: 'round'
}}
]
}
};
$.jqplot('myChart', [data], options);
});
图 9-31 显示了在值 70 和 145 之间界定线图的两条极限线。
图 9-31。
A line chart with lower and upper limits
您已经看到了如何在两条界限线之间划定折线图,在更复杂的情况下(但不是在这种情况下,这种情况不太常见),可以方便地改变这些界限的值,从而能够随意移动它们,例如,通过单击一系列按钮。在下一个示例中,您将继续通过添加按钮来实现当前图表,这些按钮用于滑动图表表面上的限制线。
向图表添加按钮
使用前面的例子(见清单 9-35),你将看到如何在图表中添加按钮。按钮可以放在网页的任何部分,因为它们在画布之外。在这里,它们的功能是允许您随意移动限制线,只需单击它们即可。
为此,您需要四个按钮:两个用于向上移动限制线,两个用于向下移动限制线,标记如下:
- 下限上限
- 下限下降
- 上限向上
- 上限下降
你可以在网页的<body>
部分的任何地方添加清单 9-36 中定义的四个按钮。
清单 9-36。ch9_16.html
<div>
<button onclick="lineup(myPlot, 'lowlimit')">Low Limit Up</button>
<button onclick="linedown(myPlot, 'lowlimit')">Low Limit Down</button>
</div>
<div>
<button onclick="lineup(myPlot, 'hilimit')">High Limit Up</button>
<button onclick="linedown(myPlot, 'hilimit')">High Limit Down</button>
</div>
这些行将生成如图 9-32 所示的四个按钮。
图 9-32。
The buttons added to the chart in order to move the limit lines
在第二章中,向您介绍了可以用作控件的 JQuery 用户界面库(jQuery UI)小部件。考虑到这种类型控件的潜力,建议使用库提供的按钮部件(关于如何使用这些部件的更多信息,参见第一章 5)。如果您希望使用 jQuery UI 小部件来替换这四个按钮,那么您需要包括以下插件:
<link rel="stylesheet" href="
http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css
>
<script src="
http://code.jquery.com/ui/1.10.3/jquery-ui.min.js
但是,如果您希望引用本地安装的库(参见附录 A),则必须包含以下代码:
<link rel="stylesheet" href="../src/css/smoothness/jquery-ui-1.10.3.custom.min.css" />
<script src="../src/js/jquery-ui-1.10.3.custom.min.js"></script>
并且,在 HTML 页面的<body>
部分,添加清单 9-37 中的代码。
清单 9-37。ch9_16.html
<script>
$(function() {
$('button')
.button()
.click(function( event ) {
event.preventDefault();
});
});
</script>
按钮现在以 jQuery UI 风格显示(或者,更准确地说,以“平滑度”为主题,这是其中之一),如图 9-33 所示。
图 9-33。
The same four buttons, but displayed using the jQuery UI
此时,这些按钮完全处于非活动状态。你需要开发两个 JavaScript 函数;当按钮被按下时,这些将被执行。第一个函数lineup()
将增加作为参数传递的线的 y 值(上限或下限),然后强制绘制新的图表。第二个函数linedown()
将减少 y 值。这两个函数必须在 jQuery 函数$(document).ready()
的外部(见清单 9-38)。
清单 9-38。ch9_16.html
function lineup(plot, name) {
var co = plot.plugins.canvasOverlay;
var line = co.get(name);
line.options.y += 5;
co.draw(plot);
}
functionlinedown(plot, name) {
var co = plot.plugins.canvasOverlay;
var line = co.get(name);
line.options.y -= 5;
co.draw(plot);
}
下一步是将由$.jqplot()
函数返回的对象赋给一个变量:
myPlot = $.jqplot('myChart', [data], options);
Note
注意不要写varmyPlot
,否则当你按下按钮时,你将看不到图表的任何变化。
最后一步是命名canvasOverlay
对象中的两条线,如清单 9-39 所示。
清单 9-39。ch9_16.html
objects: [
{horizontalLine: {
name: 'lowlimit',
y: 70,
lineWidth: 3,
color: 'rgb(255, 0, 0)',
shadow: true,
lineCap: 'butt'
}},
{dashedHorizontalLine: {
name: 'hilimit',
y: 145,
lineWidth: 4,
color: 'rgb(0, 0, 255)',
shadow: false,
dashPattern: [8, 16],
lineCap: 'round'
}}
]
最后,你得到一个包含四个按钮的图表,如图 9-34 所示。
图 9-34。
A line chart with a set of buttons that vary the lower and upper thresholds
有时,您需要在图表中添加垂直线,尤其是当您必须标记截止日期时。在这种情况下,我们将像以前一样工作,但有一些不同。例如,假设您想要在折线图中放置一条竖线来表示截止日期。在这种情况下,您使用清单 9-40 中的代码。
清单 9-40。ch9_17.html
$(document).ready(function(){
var data = [100, 110, 140, 130, 80, 75, 120, 130, 100];
var options = {
canvasOverlay: {
show: true,
objects: [
{verticalLine: {
name: 'lowlimit',
x: 5,
lineWidth: 3,
color: 'rgb(50, 200, 50)',
shadow: true,
lineCap: 'butt',
yOffset: 0
}}
]
}
};
myPlot = $.jqplot('myChart', [data], options);
});
这一次,您将只需要两个按钮。
<div>
<button onclick="lineright(myPlot, 'lowlimit')">Postpone Deadline</button>
<button onclick="lineleft(myPlot, 'lowlimit')">Anticipate Deadline</button>
</div>
现在,您必须开发两个 JavaScript 函数,它们将在按钮被按下时水平移动界限线。像前面看到的 JavaScript 函数一样,清单 9-41 中的两个函数必须放在 jQuery 函数$(document).ready()
的外部。
清单 9-41。ch9_17.html
functionlineright(plot, name) {
var co = plot.plugins.canvasOverlay;
var line = co.get(name);
line.options.x += 1;
co.draw(plot);
}
functionlineleft(plot, name) {
var co = plot.plugins.canvasOverlay;
var line = co.get(name);
line.options.x -= 1;
co.draw(plot);
}
结果就是图 9-35 中的图表,中间有一条绿色竖线。通过点击这两个按钮,如果你想提前,线条会向左移动,如果你想推迟,线条会向右移动。
图 9-35。
A line chart with a green horizontal limit line
变焦
通常,当您处理大量数据时,图表上会出现一条由数千个点组成的线。正是在这种情况下,变焦功能是必不可少的。从宏观视图开始,您可以放大线的一部分以获得数据的微观视图。
光标插件还支持绘图缩放功能。通过在图上单击并拖动光标,可以放大和滚动图表的小部分。如果双击,可以全部重置,回到宏观视图。因此,您需要在您的 web 页面中包含光标插件,并且因为您在 x 轴上有日期值,所以还必须包含 DateAxisRenderer 插件:
<script type="text/javascript"
src="../src/plugins/jqplot.dateAxisRenderer.min.js"></script>
<script type="text/javascript" src="../src/plugins/jqplot.cursor.min.js"></script>
或者,如果您更喜欢使用 CDN 服务,您可以按如下方式操作:
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins
/jqplot.dateAxisRenderer.min.js"></script>
<script type="text/javascript"
src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.cursor.min.js
清单 9-42 说明了大量输入数据的可用性。
清单 9-42。ch9_18.html
var data = [["6/22/2012 10:00:00", 110.32], ["6/8/2012 10:00:00", 115.84],
["5/26/2012 10:00:00", 121.23], ["5/11/2012 10:00:00", 122.12],
["4/27/2012 10:00:00", 120.69], ["4/13/2012 10:00:00",123.24],
["3/30/2012 10:00:00", 116.78], ["3/16/2012 10:00:00", 115.16],
["3/2/2012 10:00:00", 113.57], ["2/17/2012 10:00:00", 120.45],
["2/2/2012 10:00:00", 121.28], ["1/20/2012 10:00:00", 124.7],
["1/5/2012 10:00:00", 130.07], ["12/22/2011 10:00:00", 129.36],
["12/8/2011 10:00:00", 130.76], ["11/24/2011 10:00:00", 133.96],
["11/10/2011 10:00:00", 140.02] ,["10/27/2011 10:00:00", 138.36],
["10/13/2011 10:00:00", 140.54], ["9/29/2011 10:00:00", 140.91],
["9/15/2011 10:00:00", 140.15], ["9/2/2011 10:00:00", 138.25],
["8/25/2011 10:00:00", 137.29], ["8/11/2011 10:00:00", 139.15],
["7/28/2011 10:00:00", 144.86], ["7/14/2011 10:00:00", 145.32],
["6/30/2011 10:00:00", 148.12], ["6/16/2011 10:00:00", 146.43],
["6/2/2011 10:00:00", 147], ["5/19/2011 10:00:00", 144.62],
["5/5/2011 10:00:00", 143.2], ["4/21/2011 10:00:00", 144.06],
["4/7/2011 10:00:00", 137.45], ["3/24/2011 10:00:00", 138.08],
["3/10/2011 10:00:00", 137.92], ["2/25/2011 10:00:00", 131.18],
["2/11/2011 10:00:00", 129.64], ["1/28/2011 10:00:00", 133.9],
["1/14/2011 10:00:00", 134.25], ["12/31/2010 10:00:00", 137],
["12/17/2010 10:00:00", 136.69], ["12/3/2010 10:00:00", 144.87],
["11/19/2010 10:00:00", 146.7], ["11/5/2010 10:00:00", 143.97],
["10/22/2010 10:00:00", 139.6], ["10/8/2010 10:00:00", 133.39],
["9/24/2010 10:00:00", 130.27], ["9/10/2010 10:00:00", 132.75],
["8/27/2010 10:00:00", 130.25]];
启用缩放功能非常简单。只需在options
中将zoom
属性设置为'true'
,如清单 9-43 所示。
清单 9-43。ch9_18.html
var options = {
series: [{
neighborThreshold: -1
}],
axes:{
xaxis:{
renderer: $.jqplot.DateAxisRenderer,
min:'August 1, 2010 16:00:00',
tickInterval: '6 months',
tickOptions: {formatString: '%#m/%#d/%Y'}
}
},
cursor:{
show: true,
zoom: true,
showTooltip: false
}
};
myPlot = $.jqplot('myChart', [data], options);
或者,如果您愿意,您可以禁用重置缩放的双击。光标插件还通过在外部使用resetZoom()
方法来扩展绘图对象(由$.jqplot()
函数返回的值)。此外,可以从用户代码或另一个 HTML 元素(如按钮)调用此方法来重置绘图缩放。
您可以在 jQuery ready()
函数中定义这个函数:
$('.button-reset').click(function() { myPlot.resetZoom() });
然后,在您想要的任何位置插入以下行,以便将按钮放在网页的<body>
部分:
<button class="button-reset">Reset Zoom</button>
图 9-36 提供了代表不同时刻的折线图的一系列图片。第一张图片是从浏览器显示的折线图,没有任何缩放。第二张图片显示了用户选择的图表区域,目的是进行缩放。最后一张图片展示了这种缩放的结果。如果用户单击重置缩放按钮,浏览器将再次显示第一张图片。
图 9-36。
A detail of the line chart extracted by zooming
更改图表外观
多亏了它的几个插件,jqPlot 可以直接在画布上呈现图表组件,包括文本。到目前为止,很明显 jqPlot 库的亮点是可以通过改变 jqPlot 属性和添加的插件的默认值来改变任何图表元素的外观。但是,这不是实现这种改变的唯一方法。如果您想要修改 HTML 页面中元素的外观,您可以求助于 CSS 样式。即使对于 jqPlot 元素也是如此。
可以用 CSS 类引用几个(但不是全部)jqPlot 对象,以改变这些对象的样式,而不必在options
中设置它们的属性。对象可以由 CSS 定制,使用 CSS 类,如.jqplot-*
。
自定义文本,使用 CSS
jqPlot 库提供了 CSS 类,使用这些类,您可以在不引用options
对象的情况下更改一些属性。举例来说,您将使用其中一些类来更改图表中的文本。让我们从实现一个简单的多系列折线图开始,它只有一个标题和一个在options
中定义的轴标签(见清单 9-44)。
清单 9-44。ch9_10a.html
$(document).ready(function(){
var data1 = [1, 2, 3, 2, 3, 4];
var data2 = [3, 4, 5, 6, 5, 7];
var data3 = [5, 6, 8, 9, 7, 9];
var data4 = [7, 8, 9, 11, 10, 11];
var options = {
title: 'Multiseries Line Chart,
axesDefaults: {
label: 'Axis Label'
}
};
$.jqplot('myChart',[data1, data2, data3, data4], options);
});
您在清单 9-45 中添加了<style>
部分,它可以被提取为一个 CSS 文件。
清单 9-45。ch9_10a.html
<style>
.jqplot-title {
font-family: "Arial Black";
font-size: 24px;
color: lightblue;
}
.jqplot-xaxis-label {
font-size: 24px;
}
.jqplot-axis {
font-family: "Arial";
font-size: 16px;
}
.jqplot-xaxis {
color: green;
}
.jqplot-yaxis {
color: orange;
font-weight: bold;
}
</style>
图 9-37 展示了清单 9-45 中 CSS 样式设置前后的情况,让我们看到了所做的改动。
图 9-37。
Some CSS styles applied to the tick labels and title
更改背景颜色
继续上一个例子(见清单 9-45),你现在发现只要简单地添加一个属性到options
(在清单 9-46 中突出显示),你就可以获得一个黑色背景。如图 9-38 所示。
清单 9-46。ch9_10b.html
var options = {
title: 'Multiple Data Arrays',
axesDefaults: {
label: 'Axis Label'
},
grid: {
background: '#000000'
}
};
图 9-38。
A line chart with a black background
使用 CSS 进一步定制
这一次,您不仅要更改网格的背景,还要更改图表周围的空间,使其更具吸引力。您可以通过将 CSS 样式直接应用于图表元素来实现这一点。
例如,与默认设置(灰色网格,白色背景)相反,假设您决定将图表放置在全黑背景上。在这种情况下,您需要创建一个容器,在myChart
目标(另一个<div>
元素)中包含一个<div>
元素:
<div class="chart-container">
<div id="myChart" style="height:400px; width:500px;"></div>
</div>
该容器用于扩展将放置黑色背景的区域;我们通过用chart-container
设置其类来引用容器。
此时,要记住的最重要的事情是,容器和目标这两个元素现在可以通过更改它们的 CSS 样式来适当地表征。这可以通过为.chart-container
指定属性来实现,如清单 9-47 所示(至于目标的元素,已经使用.jqplot-*
类设置好了)。在.chart-container
类中,你将background
属性设置为'black'
;容器的大小由width
和height
属性确定。您还可以使用padding
属性来更好地将目标放在容器的中心。填充清除元素内容周围的区域,扩展其背景色。这四个值分别是顶部、右侧、底部和左侧填充。
清单 9-47。ch9_20.html
<style type="text/css">
.chart-container {
background : #000000;
padding: 30px 0px 80px 30px;
width: 560px;
height: 330px;
}
...
</style>
容器和目标的 CSS 定制的组合结果如图 9-39 所示。
图 9-39。
A multiseries line chart with a black background
设置网格
默认情况下,图表的网格是灰色的。然而,在前面的例子中(见清单 9-47),你看到了如何通过设置options
中grid
对象的属性来改变网格。在本例中,您将继续修改相同的多系列折线图,但这一次,您将关注网格属性。
您可以更改网格颜色和厚度。例如,您可能想要一个黑色的网格,增加厚度,在这种情况下,您必须定义gridLineColor
和gridLineWidth
属性。此外,有时,默认情况下,jqPlot 可能会用太粗的网格显示图表,这可能会妨碍而不是有助于可读性。在这种情况下,您需要减少分笔成交点的数量。这很容易做到,通过以特定的方式为options
中的axes
对象内的每个轴设置numberTicks
属性。清单 9-48 包括了所有这些变化。
清单 9-48。ch9_10d.html
var options = {
title: 'Multiseries Line Chart',
轴任务:{
label: 'AxisLabel'
}
,
grid: {
background: '#000000'
,
gridLineColor: '#ffffff',
gridLineWidth: 2
},
axes: {
xaxis: {
numberTicks: 5,
min: 0,
max: 8
},
yaxis: {
numberTicks: 3,
min: 0,
max: 12
}
}
};
最后,你会得到一个带有所需网格的新图表(见图 9-40 )。
图 9-40。
A multiseries line chart with a customized grid
请注意,有一个灰色的轮廓界定了图表:边界。你也可以改变它,或者禁用它,如清单 9-49 所示。您也可以将drawBorder
属性设置为'false'
并禁用shadow
。
清单 9-49。ch9_20e.html
grid: {
drawBorder: false,
shadow: false,
gridLineColor: '#000000',
gridLineWidth: 2,
},
在这些改变之后,你获得了一个可读性更好的网格,如图 9-41 中的网格,它有一个与网格颜色相同的边框(白色)。
图 9-41。
A more readable multiseries line chart, with a customized grid
使用折线图上的区域
到目前为止,您已经看到了折线图基本上由线条连接的点集合组成,描述了一定规模的趋势。现在,你可能会发现这样的观点有些局限。通常,折线图最有趣的部分是一条线(或几条线)以某种方式划定的区域。
面积图
折线图可以转换成面积图。在这个例子中,你将使用你已经创建的多系列折线图(见清单 9-50),为了得到一个混合了区域和线条的新图表,你需要进行一些修改。在这里,您将看到只需做很少的更改就能达到预期的效果。
清单 9-50。ch9_22a.html
$(document).ready(function(){
var data1 = [1, 2, 3, 2, 3, 4];
var data2 = [3, 4, 5, 6, 5, 7];
var data3 = [5, 6, 8, 9, 7, 9];
var data4 = [7, 8, 9, 11, 10, 11];
var options = {
title:'Multiple Data Arrays'
};
$.jqplot ('myChart', [data1, data2, data3, data4], options);
});
首先,在options
中,您必须将fill
属性插入到您想要表示为区域的系列中。这一次,您选择seriesDefaults
将区域表示应用于所有系列。这样,你就得到一个面积图。为了使图表更好,你可以添加其他选项,比如平滑(见清单 9-51)。
清单 9-51。ch9_22a.html
var options = {
title: 'Multiple Data Arrays',
seriesDefaults: {
showMarker: false,
rendererOptions: {
smooth: true
},
fill: true
}
};
但是,当运行该网页时,您立即发现有些不对劲:最后一个系列的区域覆盖了其他系列(见图 9-42 )。
图 9-42。
An area chart with one series covering the others
在对序列应用fill
属性之前,您需要考虑序列中相应区域的表示顺序。在这种情况下,只需以不同的方式对序列进行排序:
$.jqplot ('myChart', ``data4, data3, data2, data1
结果是一个精确的面积图,如图 [9-43 所示。
图 9-43。
A multiseries area chart rendered correctly
折线图和面积图
在同一个图表中混合线条和区域也可以创造出非常好的效果。继续上一个例子(见清单 9-51),不要在seriesDefaults
中设置fill
属性,然后对所有序列应用填充,你可以决定逐个序列地这样做,以选择哪个序列必须表示为面积,哪个序列必须表示为线条,如清单 9-52 所示。
清单 9-52。ch9_22b.html
var options = {
title:'Multiple Data Arrays',
seriesDefaults: {
showMarker: false,
rendererOptions: {
smooth: true
}
},
series: [{}, {fill: true}, {}, {fill: true}]
};
图 9-44 显示了如何组合折线图和面积图。
图 9-44。
A combined line and area chart
波段图
带状图(也称为高低点折线图或范围图)是一种结合了面积图和折线图特征的图表。
带状图是一种增强了底层阴影区域的折线图(见图 9-45 )。该区域代表 y 轴上数值范围的上限和下限。这个范围随着 x 的变化而变化,最终,你会得到一个波段。
图 9-45。
A band chart
您可以使用一个带区来指示 y 轴上的特定区间,该区间随 x 轴上的值而变化,并与其内部的线条趋势相关联,例如,说明置信区间或误差带。另一个用途可能是突出显示随时间变化的分布和显示算术平均值的线。
使用 jqPlot,可以自动计算或手动分配波段。如果手动分配,则必须以两个[x,y]值数组的形式提供波段的边界。第一个数组限定了下限线;第二个数组,上界线。这两个数组作为另一个数组的两个元素连接在一起,传递给options
中的bandData
属性。
首先,让我们用[x,y]值对和波段数组bdata
定义一个数据数组,包含两个数组:下界线和上界线(见清单 9-53)。
清单 9-53。ch9_24a.html
var data = [[10,100],[20,110],[30,140],[40,130],
[50,80],[60,75],[70,120],[80,130],[90,100]];
varbdata =[ [[10,90],[20,100],[30,130],[40,120],
[50,70],[60,65],[70,110],[80,120],[90,90]],
[[10,110],[20,120],[30,150],[40,140],
[50,90],[60,85],[70,130],[80,140],[90,110]]];
然后,在options
中,使用清单 9-54 所示的代码。图 9-46 展示了由此产生的带状线图。
清单 9-54。ch9_24a.html
var options = {
series: [{
rendererOptions: { bandData: bdata }
}],
seriesDefaults: {
shadow: false,
showMarker: false
}
};
$.jqplot ('myChart', [data], options);
图 9-46。
A banded-line chart
如果您选择绘制平滑折线图,波段也会变得平滑。清单 9-55 给出了代码,图 9-47 给出了结果。
清单 9-55。ch9_24b.html
rendererOptions: {
bandData: bdata,
smooth: true
}
图 9-47。
A smooth-banded-line chart
波段数据数组中的点数不必与数据序列中的点数相对应。此外,如果数据系列经过平滑处理,波段数据将被绘制为平滑线。该带不必相对于主线对称。通过在bdata
数组中插入一个具有不对称 y 值的数组,可以使波段不对称,如清单 9-56 所示。
清单 9-56。ch9_24c.html
varbdata =[ [[10,90],[30,100],[40,100],[50,70],
[60,65], [70,110],[80,120],[90,90]],
[[10,110],[30,150],[40,140],[50,120],
[60,85], [70,130],[80,140],[90,110]] ];
现在,图 9-48 中的带相对于主线不对称。
图 9-48。
A nonuniform banded-line chart
但是,提供波段数据不是强制性的;它们可以由 jqPlot 自动计算。要激活这个特性而不使用任何数组,你必须将bands
对象的show
属性设置为rendererOptions
中的'true'
,如清单 9-57 所示。如图 9-49 所示,默认情况下,波段间隔覆盖主线 y 值的+/-3%。
清单 9-57。ch9_24d.html
series: [{
rendererOptions: {
bands: { show: true},
smooth: true
}
}],
图 9-49。
A banded-line chart with a band interval of +/-3 percent
填充折线图中的线条
你刚刚了解了乐队。为什么不填充两条系列线之间的区域?甚至这个任务也可以用 jqPlot 来完成。通过设置fillBetween
对象内的属性,可以控制图上两条线之间的区域。
在这里,你从一个非常简单的多系列折线图开始(同样的例子用于其他情况),如清单 9-58 所示。
清单 9-58。ch9_5a.html
$(document).ready(function(){
var data1 = [1, 2, 3, 2, 3, 4];
var data2 = [3, 4, 5, 6, 5, 7];
var data3 = [5, 6, 8, 9, 7, 9];
var data4 = [7, 8, 9, 11, 10, 11];
var options = {
title:'Multiple Data Arrays',
};
$.jqplot('myChart', [data1, data2, data3, data4], options);
});
使用此多系列折线图,您可以考虑每个系列,第一个系列的索引从 0 开始;1、为第二系列;2、为第三;诸如此类。
所以,如果你想填充两行之间的区域,你需要在series1
和series2
属性中指定与它们对应的两个索引。例如,如果你想填充第二个和第四个系列之间的区域,你必须设置series1
为 1(第二个系列)和series 2
为 3(第四个系列),如清单 9-59 所示。可选地,您可以使用color
属性或者更好地使用rgba()
函数来设置分隔区域的颜色。
清单 9-59。ch9_25.html
var options = {
title: 'Multiple Data Arrays',
fillBetween: {
series1: 1, //second series
series2: 3, //fourth series
color: "rgba(10, 120, 130, 0.7)"
}
});
在图 9-50 中,您可以看到第二个和第四个系列之间的选定区域是彩色的。
图 9-50。
A multiseries line chart with a colored area between two lines
您可以将一个 JavaScript 函数绑定到一个按钮,用于更新每个系列的绘图设置,然后重新绘制所有内容。为此,让我们将清单 9-60 中的函数添加到 jQuery ready()
函数中。
清单 9-60。ch9_26.html
$("button[name=changeFill]").click(function(e) {
plot1.fillBetween.series1 = parseInt($("input[name=series1]").val());
plot1.fillBetween.series2 = parseInt($("input[name=series2]").val());
plot1.replot();
});
为了让前面的 JavaScript 函数工作,您需要将函数$.jqplot()
返回的值赋给变量plot1
:
plot1 = $.jqplot ('myChart', [data1, data2, data3, data4], options);
并且,在网页的<body>
部分,你必须添加两个输入文本区域和一个按钮,如清单 9-61 所示。
清单 9-61。ch9_26.html
<label for="series1">First Series: </label>
<input type="text" name="series1" value="1" />
<label for="series2"> Second Series: </label>
<input type="text" name="series2" value="3" />
<button name="changeFill">Change Fill</button>
结果如图 9-51 所示。
图 9-51。
A multiseries line chart with a selectable colored area
要用一个 jQuery UI 小部件替换简单的 HTML 控件,你必须对代码做一些修改以便集成它,如清单 9-62 所示。
清单 9-62。ch9_26ui.html
<link rel="stylesheet" href="../src/css/smoothness/jquery-ui-1.10.3.custom.min.css" />
<script src="../src/js/jquery-ui-1.10.3.custom.min.js"></script>
...
$("button[name=changeFill]").click(function(e) {
plot1.fillBetween.series1 = parseInt($("#combobox").val());
plot1.fillBetween.series2 = parseInt($("#combobox2").val());
plot1.replot();
});
...
<div class="ui-widget">
<label>First Series : </label>
<select id="combobox">
<option value="0">1</option>
<option value="1">2</option>
<option value="2">3</option>
<option value="3">4</option>
</select>
</div>
<div class="ui-widget">
<label>Second Series : </label>
<select id="combobox2">
<option value="0">1</option>
<option value="1">2</option>
<option value="2">3</option>
<option value="3">4</option>
</select>
</div>
<button name="changeFill">Change Fill</button>
<script>
$(function() {
$( 'button')
.button()
.click(function( event ) {
event.preventDefault();
});
});
</script>
结果就是图 9-52 所示的图表。
图 9-52。
A multiseries line chart with a selectable colored area
趋势线
jqPlot 真的是惊喜满满。除了你已经看到的关于折线图的所有东西,jqPlot 还可以计算和表示趋势线。这些通常是在图表中绘制的直线,但有时它们可以是指数的(如果它们在对数标度中是线性的)。趋势线表示图表中绘制的系列数据的一般模式或方向。这条线是用统计技术画出来的。这个功能由另一个插件执行:Trendline。
要启用此功能,您需要在网页中包含插件:
<script type="text/javascript" src="../src/plugins/jqplot.trendline.min.js"></script>
之后,您只需要激活插件,添加启用它的行,如清单 9-63 所示。
清单 9-63。ch9_27a.html
$(document).ready(function(){
var data = [100, 110, 140, 130, 135, 132, 140, 135, 142]
$.jqplot.config.enablePlugins = true;
$.jqplot ('myChart', [data]);
});
用这几条线可以得到一条趋势线,如图 9-53 所示。
图 9-53。
The linear trend line of a line chart
但是,如果您喜欢显式地表达这些属性,您可以通过使用选项来实现。这使您能够像处理图表中的其他对象一样处理趋势线。假设你想改变线条的颜色,增加线条的粗细,使其更加突出(见清单 9-64)。
清单 9-64。ch9_27b.html
var options = {
seriesDefaults: {
trendline: {
show:true,
color: '#ff0000',
lineWidth: 4
}
}
}
$.jqplot ('myChart', [data], options);
你现在对趋势线有了更多的控制。图 9-54 显示了trendline
对象中带有属性设置的线条(趋势线较粗,在浏览器上显示为深红色)。
图 9-54。
A customized linear trend line in a line chart
如前所述,可以使用趋势线曲线,它表示图表中各点之后的指数趋势。让我们在清单 9-65 的下一个例子中检验这一点。
清单 9-65。ch9_28.html
$(document).ready(function(){
var data = [[10, 1.44], [30, 6.98], [50, 10.7], [70, 37.5], [90, 78.1]];
var options = {
seriesDefaults: {
trendline: {
show:true,
color: '#ff0000',
lineWidth: 4,
type: 'exponential'
}
}
}
$.jqplot ('myChart', [data], options);
});
图 9-55 显示了一个折线图,其中绘制了一系列遵循指数趋势的点。因此,你可以用指数趋势线来强调这一点。
图 9-55。
A customized exponential trend in a line chart
摘要
在这丰富的章节中,你已经精通 jqPlot 世界。您看到了这个库提供的许多可能性,使您能够尽最大能力实现折线图。您学习了如何操作绘制图表的基本元素,如轴和刻度。特别是,您看到了如何在同一个图表中管理多个数据系列(多系列图表),添加各种图形效果。您还探索了 jqPlot 库允许您操作不同格式的日期和时间值的方式。此外,您看到了如何使用 HTML 格式定制一些元素,以及突出显示数据点。在本章的最后一部分,你处理了更复杂的情况,比如生成趋势线和使用波段图。
在下一章,一个充满争论的章节,你将会面对其他新的概念,这次是应用在条形图上。
十、jqPlot 条形图
Abstract
在这一章中,你将处理另一大类图表:条形图。在前一章中,您已经了解了 jqPlot 中默认图表类型——折线图的特征。现在,使用 BarRenderer 插件,您将发现主 jqPlot 对象的结构是如何随着新的属性和对象而逐渐丰富的。通过实际例子,你会看到如何用rendererOptions
改变属性和对象属性的值。
在这一章中,你将处理另一大类图表:条形图。在前一章中,您已经了解了 jqPlot 中默认图表类型——折线图的特征。现在,使用 BarRenderer 插件,您将发现主 jqPlot 对象的结构是如何随着新的属性和对象而逐渐丰富的。通过实际例子,你会看到如何用rendererOptions
改变属性和对象属性的值。
有时,使用同一组数据可以获得不同的表示。学习如何选择最适合你需求的表现形式是本书的基本目标之一。为此,使用一组数据,您将看到如何从分组条形图切换到堆叠条形图,在这两种情况下都可以在垂直和水平表示之间进行选择。
此外,您将了解如何使用 jqPlot 库来表示组合图表,例如,如何同时表示折线图和条形图,以及如何通过降低绘图速度来获得简单但引人注目的动画。,您还将熟悉一种特殊类型的条形图,Marimekko 图,它是由 jqPlot 库以非常令人满意的方式实现的。
在本章的最后一部分,我将介绍 jqPlot 中事件的使用。这是一个复杂的主题,但是由于特殊的 jQuery 函数,您只需几行代码就可以实现显著的交互效果。本章最后给出了这方面的一个典型例子:如何定制工具提示。
使用 BarRenderer 插件创建条形图
当您有一组分为不同类别的数据,并且需要将这些类别相互比较时,条形图可能是最适合您需求的表示形式。您已经看到,在不包含任何插件的情况下,默认情况下传入的数据被解释为连接起来形成一条线的点。为了告诉 jqPlot 输入的数据必须用于绘制条形图,您必须在 HTML 页面的<head>
部分放置一组插件:
<script type="text/javascript" src="../src/plugins/jqplot.dateAxisRenderer.min.js"></script>
<script type="text/javascript"
src="../src/plugins/jqplot.canvasAxisTickRenderer.min.js"></script>
<script type="text/javascript" src="../src/plugins/jqplot.categoryAxisRenderer.min.js"></script>
<script type="text/javascript" src="../src/plugins/jqplot.barRenderer.min.js"></script>
或者,如果您更喜欢使用内容交付网络(CDN)服务,您可以按照以下方式进行:
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.dateAxisRenderer.min.js
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.canvasAxisTickRenderer.min.js
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.categoryAxisRenderer.min.js
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.barRenderer.min.js
要设置输入数据以便在条形图中使用,您必须插入一个格式为[label,y]的数组,其中 x 不再出现,但一个指示性标签取而代之。这个标签通常是一个字符串值。事实上,当我们谈论条形图时,我们不再对跟踪一个变量(y 值)相对于另一个变量(x 值)的趋势感兴趣,而是对比较数据(标签)的类别或组感兴趣。对于此示例,您将使用五个组,每个组代表一个州,作为标签报告:
var data = [['Germany', 12], ['Italy', 8], ['Spain', 6], ['France', 10], ['UK', 7]];
一旦包含了 BarRenderer 插件,就必须激活它,将它的引用分配给series
对象中的renderer
属性(参见清单 10-1)。您将对第二个插件 CategoryAxisRenderer 做同样的事情,只为xaxis
对象指定它。
清单 10-1。ch10_01a.html
var options = {
title: 'Foreign Customers',
series:[{renderer:$.jqplot.BarRenderer}],
axes: {
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer
}
}
};
$.jqplot ('myChart', [data], options);
接下来,在 HTML 页面的<body>
部分,添加以下行:
<div id="myChart" style="height:300px; width:500px;"></div>
这样,你就得到一个简单的条形图,如图 10-1 所示。数据数组中包含的每个状态都由一个蓝色条表示,其高度对应于 y 值。
图 10-1。
A simple bar chart
旋转轴刻度标签
通常,可能需要或希望旋转 x 轴上报告的刻度标签。例如,文本可能太长而无法报告,为了保持标签的可读性,您需要将它们写成以一定角度倾斜。这种旋转是通过包含 CanvasTextRenderer 插件实现的:
<script type="text/javascript" src="../src/plugins/jqplot.canvasTextRenderer.min.js"></script>
或者,如果您更喜欢使用 CDN 服务,您可以按如下方式操作:
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.canvasTextRenderer.min.js
></剧本>
这里,简单地包含插件是不够的;您还必须通过将它的引用传递给tickRenderer
属性来激活它,如清单 10-2 所示。然后,您需要在tickOptions
中指定某些属性:您将angle
属性设置为–30 度。使用此值,您可以指示文本相对于 x 轴的倾斜度。如果该值为正值,文本将顺时针方向旋转;如果为负(如清单 10-2 所示),则逆时针旋转。
清单 10-2。ch10_01b.html
var options = {
title: 'Foreign customers',
series:[{ renderer: $.jqplot.BarRenderer }],
axes: {
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer,
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
tickOptions: {
angle: -30,
fontSize: '10pt'
}
}
}
};
$.jqplot ('myChart', [data], options);
如果您现在在浏览器中加载网页,在图表的底部,您会看到所有标签都相对于 x 轴逆时针旋转(见图 10-2 )。
图 10-2。
A bar chart with rotated labels on the x axis
修改条形之间的间距
使用条形图时,最常见的需求可能是改变条形之间的间距。这个间距可以通过设置不同值的barMargin
属性来直接调整。因为这个属性不属于jqplot
对象,而是特定于 BarRenderer 插件,所以您必须在rendererOptions
中指定它。每当您包含一个渲染器插件时,您也包含了一组不属于原始jqplot
对象的全新属性。所以,如果你想要一个不同于这些属性的默认值,你需要在rendererOptions
中写这个属性,设置新的值。例如,让我们在条形之间应用一个 30 像素的空间,如清单 10-3 所示。
清单 10-3。ch10_02.html
var options = {
title: 'Foreign Customers',
seriesDefaults:{
renderer:$.jqplot.BarRenderer,
rendererOptions: {
barMargin: 30
},
},
axes: {
因为图表的宽度保持不变,作为将barMargin
属性增加到 30 的结果,所有的条形都比以前窄了(见图 10-3 )。
图 10-3。
The space between bars is adjustable with a new property introduced by the plug-in
在条形顶部添加值
jqPlot 库允许您处理偶数点标签。虽然您也可以在折线图中使用它们,但是点标签是条形图的重要组成部分。如果激活点标签,将在条形上方明确显示 y 值,从而增强值的可读性,尤其是对于堆积条形图。要激活此功能,您需要包括另一个插件:
<script type="text/javascript" src="../src/plugins/jqplot.pointLabels.min.js"></script>
或者,如果您更喜欢使用 CDN 服务,您可以按如下方式操作:
<script type="text/javascript"
src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.pointLabels.min.js
您可能已经注意到 PointLabels 不是渲染器插件,因此它已经处于活动状态。这一次,不需要在renderer
属性中传递引用。这个过程非常简单快捷:在options
中,你将pointLabels
对象的show
属性设置为'true'
(见清单 10-4)。
清单 10-4。ch10_03.html
seriesDefaults:{
renderer:$.jqplot.BarRenderer,
pointLabels: { show: true }
},
如图 10-4 所示,激活点标签后,y 值将出现在每个条的上方。
图 10-4。
A bar chart reporting the y value above each bar
具有负值的条形
一般来说,我们习惯于看到所有 y 值都为正的条形图,但情况并非总是如此。但是,如果您想在条形图上表示负值,就必须小心。如果您尝试使用包含负值的输入数据数组,如下例所示
var data = [['Germany', -12], ['Italy', -8], ['Spain', -6], ['France', -10], ['UK', -7]];
你得到了图 10-5 中的条形图。
图 10-5。
This bar chart has interpreted the negative values badly
这不是你真正想要的。这些条被画出来,好像它们仍然是正的。只有点标签正确显示 y 值。此外,y 轴上报告的值与条形的表示不相关,条形应该从顶部开始,向下到 y 轴上相应的负值。为了克服所有这些问题,您需要将fillToZero
属性设置为'true'
,这是一个属于 BarRenderer 插件的属性;因此,你必须在rendererOptions
中指定这一点(见清单 10-5)。
清单 10-5。ch10_04a.html
var options = {
title: 'Foreign Customers',
seriesDefaults:{
renderer:$.jqplot.BarRenderer,
rendererOptions: { fillToZero: true },
pointLabels: { show: true }
},
...
现在,jqPlot 可以正确地表示负棒线(见图 10-6 )。
图 10-6。
A simple bar chart with negative values
当正值和负值都出现在同一个条形图中时,此功能更容易理解:
var data = [['Germany', -12], ['Italy', 8], ['Spain', -6], ['France', 10], ['UK', -7]];
正如你在图 10-7 中所看到的,稍微深一点的颜色区分了具有负值的条形。
图 10-7。
A simple bar chart with positive and negative values
包含多组数据的条形图
你已经看到了折线图是如何管理多个序列的,所以你可能认为条形图也有同样的可能性。在从单系列到多系列的转换中,您需要对输入数据的组织方式进行一些更改。因此,您从单个序列的输入数据数组的格式开始:
var data = [['Germany', 12], ['Italy', 8], ['Spain', 6], ['France', 10], ['UK', 7]];
首先,您必须指定一个定制的ticks
数组,它必须包含数据的组或类别的名称(您想要在 x 轴上报告的值)。刻度数应该与每个系列中 y 值的数量相匹配。
var ticks = ['Germany', 'Italy', 'Spain', 'France', 'UK'];
因为您正在处理多个系列,所以您可以指定至少三个数据系列。每个系列代表数据的进一步分类,因此您可以通过报告数据所属组的标签来区分它们。现在,你只需要为每个序列插入 y 值(来自ticks
数组),如清单 10-6 所示,假设每个序列的 x 值是相同的。
清单 10-6。ch10_05.html
var data = [12, 8, 6, 10, 7]; // Electronics customers
var data2 = [14, 12, 4, 14, 11]; // Software customers
var data3 = [18, 10, 5, 9, 9]; // Mechanics customers
关于表示系列的名称,你必须在series
对象中指定它们(见清单 10-7),将它们逐个分配给每个系列的label
属性。
清单 10-7。ch10_05.html
var options = {
title: 'Foreign Customers',
seriesDefaults:{
renderer:$.jqplot.BarRenderer,
},
series:[
{label: 'Electronics'},
{label: 'Software'},
{label: 'Mechanics'}
],
axes: {
...
现在,和往常一样,在options
中,你将ticks
数组赋给xaxis
对象的ticks
属性(见清单 10-8)。这样,您已经将 x 轴上生成的每个记号分配给数组中包含的一个字符串。
清单 10-8。ch10_05.html
axes: {
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer,
ticks: ticks
}
},
您已经看到,处理多个系列只是增加了数据的进一步分类。除了划分到 x 轴上表示的类别之外,数据还划分到多个系列中,每个系列表示一个不同的组。为了区分一个系列和另一个系列,需要用不同的颜色绘制相应的线条。但是,如果您就此打住,观察此图表的用户将不会获得任何关于哪个组由哪种颜色表示的信息。因此需要引入一个图例(见清单 10-9)。
清单 10-9。ch10_05.html
var options = {
title: 'Foreign Customers',
seriesDefaults: {
renderer: $.jqplot.BarRenderer,
},
series:[
{label: 'Electronics'},
{label: 'Software'},
{label: 'Mechanics'}
],
axes: {
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer,
ticks: ticks
}
},
legend: {
show: true,
placement: 'outsideGrid',
location: 'e'
}
};
$.jqplot ('myChart', [data, data2, data3], options);
这样你就得到一个多系列条形图,如图 10-8 所示。正如您所看到的,当您使用多个系列时,您必须使用不同的颜色来区分它们。
图 10-8。
A multiseries bar chart containing a legend
垂直和水平条形图
查看图 10-8 中的图表,您会注意到每组都用一种颜色表示。默认情况下,分配的颜色遵循 jqPlot 内部指定的顺序,该顺序反映在折线图中。每个国家在 x 轴上的区段中有三列,每个区段由网格线界定。
这种条形图一般定义为垂直条形图。没有什么可以阻止我们用水平方向的条来表示相同的输入数据,但是这里也需要对输入数据数组的格式进行修改。在这种情况下,有必要使用[y,n]对,其中n
是分配给一个字符串的整数值(见清单 10-10)。字符串是包含在ticks
数组中的标签描述,n
是它的索引。
清单 10-10。ch10_06.html
var data = [[12, 1], [8, 2], [6, 3], [10, 4], [7, 5]];
var data2 = [[14, 1], [12, 2], [4, 3], [14, 4], [11, 5]];
var data3 = [[18, 1], [10, 2], [5, 3], [9, 4], [9, 5]];
var ticks = ['Germany', 'Italy', 'Spain', 'France', 'UK'];
更改输入数据数组的格式后,必须将barDirection
属性设置为'horizontal'
( 'vertical'
是默认值)。如清单 10-11 所示,您必须在seriesDefaults
中这样做,以便将水平方向应用于所有系列。这一次,需要将ticks
数组赋给yaxis
对象中的ticks
属性,而不是像以前一样赋给xaxis
对象。
清单 10-11。ch10_06.html
var options = {
title: 'Foreign Customers',
seriesDefaults:{
renderer: $.jqplot.BarRenderer,
rendererOptions: {
barDirection: 'horizontal'
}
},
series:[
{label: 'Electronics'},
{label: 'Software'},
{label: 'Mechanics'}
],
axes: {
yaxis: {
renderer: $.jqplot.CategoryAxisRenderer,
ticks: ticks
}
},
legend: {
show: true,
placement: 'outsideGrid',
location: 'e'
}
};
$.jqplot ('myChart', [data, data2, data3], options);
现在,你得到了如图 10-9 所示的水平多系列条形图。
图 10-9。
A horizontal multiseries bar chart
垂直堆积条形图
当您需要将数据系列分解成其组成部分,同时保留将这些数据系列作为一个整体进行比较的能力时,您必须使用堆积图。jqPlot 库支持这样的图表。对于堆积条形图,添加点标签尤其合适,这样可以使图表更具可读性。报告的值是累积的,也就是堆栈中底层条形的总和,如清单 10-12 所示。
清单 10-12。ch10_07.html
var data = [12, 8, 6, 10, 7];
var data2 = [14, 12, 4, 14, 11];
var data3 = [18, 10, 5, 9, 9];
var ticks = ['Germany', 'Italy', 'Spain', 'France', 'UK'];
var options = {
title: 'Foreign Customers',
stackSeries: true,
seriesDefaults:{
renderer:$.jqplot.BarRenderer,
pointLabels: { show: true,location: 's' }
},
series:[
{label: 'Electronics'},
{label: 'Software'},
{label: 'Mechanics'}
],
axes: {
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer,
ticks: ticks
}
},
legend: {
show: true,
placement: 'outsideGrid',
location: 'e'
}
};
$.jqplot ('myChart', [data, data2, data3], options);
通过设置pointLabels
属性的值,可以指定显示点标签的位置:'n'
、's'
、' e'
、'w'
、'ne'
、'nw'
、'se'
或'sw'
。这些值应该被解释为指示绘制点标签的方向的基本点,相对于条形的顶部。在本例中,您选择's'
(south)在条形顶部下方的彩色区域显示数值,如图 10-10 所示。
图 10-10。
A vertical multiseries stacked bar chart
水平堆叠条形图
同样,您可以创建水平堆积条形图。在这种情况下,为了表示线段内的点标签,你需要将它们设置为'w'
(west)(见清单 10-13)。
清单 10-13。ch10_08.html
var data = [[12, 1], [8, 2], [6, 3], [10, 4], [7, 5]];
var data2 = [[14, 1], [12, 2], [4, 3], [14, 4], [11, 5]];
var data3 = [[18, 1], [10, 2], [5, 3], [9, 4], [9, 5]];
var ticks = ['Germany', 'Italy', 'Spain', 'France', 'UK'];
var options = {
title: 'Foreign Customers',
stackSeries: true,
seriesDefaults:{
renderer: $.jqplot.BarRenderer,
rendererOptions: {
barDirection: 'horizontal'
},
pointLabels: { show: true, location: 'w' }
},
series:[
{label: 'Electronics'},
{label: 'Software'},
{label: 'Mechanics'}
],
axes: {
yaxis: {
renderer: $.jqplot.CategoryAxisRenderer,
ticks: ticks
}
},
legend: {
show: true,
placement: 'outsideGrid',
location: 'e'
}
};
$.jqplot ('myChart', [data, data2, data3], options);
在图 10-11 中,您可以看到数值是如何显示在靠近条形末端(西部)的彩色区域。
图 10-11。
A horizontal multiseries stacked bar chart
组合图表:条形图中的线条
组合图表是在单个图表中组合两种或多种图表类型的图表。在下面的示例中,您将考虑在一个图表中同时显示条形图系列和折线图系列。对于这种表示,您需要使用一个双 y 轴。每个系列都有自己的单位和大小,因此必须符合这些轴之一。因此,你必须使用主轴和副轴。您还必须激活自动缩放功能,以便强制 y 轴对齐刻度线,从而获得一致的网格线。
因此,让我们定义两个输入序列(见清单 10-14)。数组data
包含要显示为条形图的[label1,y1]对数值。数组line
包含要显示为折线图的[label2,y2]对数值。
清单 10-14。ch10_09.html
var data = [['Germany', 12], ['Italy', 8], ['Spain' ,6], ['France', 10], ['UK', 7]];
var line = [['BMW', 45], ['AlfaRomeo', 30], ['Seat', 24],['Renault', 36], ['Mini', 30]];
这个案例有助于理解使用多个 y 轴的效用(jqPlot 最多支持九个 y 轴和两个 x 轴)。这里,您有两个序列,它们的顺序由您在函数$.jqplot()
中作为第二个参数传递它们的顺序来定义:
$.jqplot ('myChart', [data, line], options);
数组data
(用于条形图的系列)排在第一位,数组line
(用于折线图)排在第二位。这一点非常重要。建立了这个顺序后,在options
中,您需要在series
对象中指定两个元素,如清单 10-15 所示。仅在第一个元素中,您激活了 BarRenderer 插件,而在第二个元素中,您定义了两个辅助轴:x2axis
和y2axis
。现在,您有四个轴可以使用,因此,您必须在axes
对象中指定它们。在yaxis
和y2axis
上,您必须激活自动缩放。
清单 10-15。ch10_09.html
var options = {
title: 'Foreign customers',
series:[{renderer: $.jqplot.BarRenderer},
{
xaxis: 'x2axis',
yaxis: 'y2axis'
}],
axes: {
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer
},
x2axis: {
renderer: $.jqplot.CategoryAxisRenderer
},
yaxis: {
autoscale: true
},
y2axis: {
autoscale: true,
renderOptions: {
alignTicks: true
}
}
}
};
其结果是图 10-12 中的图表,同时包含条和线。
图 10-12。
A line chart combined with a bar chart
动画情节
jqPlot 库还为您提供了动画图表的能力。为此,您不需要任何额外的插件。从前面的例子开始,组合图(见清单 10-15),你可以为每个系列分配不同的速度。在定义绘图速度时,就好像您正在减慢浏览器创建图表元素的速度。这会在绘制过程中产生动态效果,从而创建动画。此外,通过分配不同的速度给不同的部分,你可以获得非常好的效果。
正如我们在清单 10-16 中看到的,在options
中,您必须通过将animate
和animateReplot
属性设置为'true'
来激活动画功能。然后,使用数值(毫秒数)为每个系列定义不同的速度。
清单 10-16。ch10_10.html
var options = {
animate: true,
animateReplot: true,
title: 'Foreign Customers',
series:[{
renderer: $.jqplot.BarRenderer,
rendererOptions: {
animation: {
speed: 2500
},
}
},{
xaxis: 'x2axis',
yaxis: 'y2axis',
rendererOptions: {
animation: {
speed: 2500
},
}
}],
axes: {
xaxis: { renderer: $.jqplot.CategoryAxisRenderer },
x2axis: {renderer: $.jqplot.CategoryAxisRenderer },
yaxis: { autoscale:true, numberTicks: 6 },
y2axis: { autoscale:true, numberTicks: 6 }
}
};
当您在浏览器中加载此图表时,您会获得一个动画,其中缓慢而平滑地绘制了一个折线图和一个条形图。图 10-13 显示了动画如何在连续的阶段中发展。折线图是按照数据点的顺序从左到右绘制的,同时,条形会增长到各自的 y 值。
图 10-13。
An animated combined line–bar chart
马里梅科海图
一种可以从条形图派生出来的图表是所谓的 Marimekko 图表(也称为 mekko 图表),因其与 Marimekko 印刷品相似而得名。这种图表已经被商业界所采用。Marimekko 图表本质上是堆叠柱形图。然而,这里所有的栅栏都是一样高的。此外,条形之间没有空间,条形被分成几段,其高度与百分比相关(见图 10-14 )。
Marimekko 图表旨在报告两个轴上的百分比值:x 轴上每个条形所在位置的每个类别所占的百分比,以及 y 轴上每个类别所占的百分比,由每个条形所划分的段表示。
图 10-14。
A Marimekko pattern
jqPlot 允许您使用两个特定的插件来开发这种图表:MekkoRenderer 和 MekkoAxisRenderer:
<script class="include" type="text/javascript"
src="../src/plugins/jqplot.mekkoRenderer.min.js"></script>
<script class="include" type="text/javascript"
src="../src/plugins/jqplot.mekkoAxisRenderer.min.js"></script>
<script class="include" type="text/javascript"
src="../src/plugins/jqplot.canvasTextRenderer.min.js"></script>
<script class="include" type="text/javascript"
src="../src/plugins/jqplot.canvasAxisLabelRenderer.min.js"></script>
或者,如果您更喜欢使用 CDN 服务,您可以按如下方式操作:
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.mekkoRenderer.min.js
></剧本>
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.mekkoAxisRenderer.min.js
></剧本>
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.canvasTextRenderer.min.js
></剧本>
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.canvasAxisLabelRenderer.min.js
></剧本>
除了这两个插件,您还需要包括 CanvasTextRenderer 和 CanvasAxisLabelRenderer。为图表中的每个条形指定数据。您可以将数据指定为 y 值数组或[标签,值]对数组。在清单 10-17 中,注意标签只用于第一个系列;后续系列的标签将被忽略。
清单 10-17。ch10_11.html
var bar1 = [['bananas', 10],['apples', 7],['pears', 4],
['peaches', 8],['lemons', 7],['oranges',5]];
var bar2 = [9, 5, 8, 11, 9, 4];
var bar3 = [11, 4, 7, 3, 8, 7];
var bar4 = [5, 8, 11, 4, 12, 3];
var barLabels = ['Italy', 'Spain', 'France', 'Greece'];
在options
中,您激活 MekkoRenderer 插件,将其分配给seriesDefaults
对象。通过将legend
对象的show
属性设置为'true'
,可以在图表的右侧添加一个图例。如果您想在 x 轴下方放置每个条形的标签,您必须将barLabels
数组分配给 x 轴上的barLabels
属性。
清单 10-18。ch10 _ 11 . html[无标注]
var options = {
title: 'Fruit Consumption in 2012',
seriesDefaults:{renderer: $.jqplot.MekkoRenderer},
legend:{show: true},
axesDefaults:{
renderer: $.jqplot.MekkoAxisRenderer
},
axes:{
xaxis:{
barLabels: barLabels,
tickOptions:{formatString: '%d'}
}
}
};
$.jqplot('myChart', [bar1, bar2, bar3, bar4], options);
现在,你得到了如图 10-15 所示的 Mekko 图。
图 10-15。
A Mekko chart
条形图事件
在条形图中,如果您将光标移动到一个条上,默认情况下它会高亮显示。当鼠标悬停在某个条上时,以及当您单击某个条时,都会触发事件。捕捉和管理这些事件的能力非常重要,jqPlot 库允许您这样做。您可以为不同类型的事件实现特定的响应操作,从而使您的图表更具交互性。您获得的响应可能取决于您找到鼠标指针的位置或您单击的目标。
表 10-1 报告的事件,由于其丰富的元素,适合在条形图中应用。让我们一个一个地看看这些事件。
表 10-1。
Handling Events with the jqPlot Library
| 事件 | 被触发时 | | --- | --- | | `jqplotDataClick` | 在数据点上单击鼠标左键。 | | `jqplotRightClick` | 用鼠标右键单击数据点。 | | `jqplotDataMouseOver` | 你把鼠标放在数据点上。 | | `jqplotDataHighlight` | 数据点被突出显示。 | | `jqplotDataUnhighlight` | 数据点未突出显示。 |jqplotDataClick 事件
本示例显示了jqplotDataClick
事件——被点击的序列索引、点及其数据值。
让我们从第一个例子开始,简单的条形图(见清单 10-19)。
清单 10-19。ch10_12.html
var data = [['Germany', 12], ['Italy', 8], ['Spain', 6],
['France', 10], ['UK', 7]];
var options = {
title: 'Foreign Customers',
series:[{renderer: $.jqplot.BarRenderer}],
axes: {
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer
}
}
};
$.jqplot ('myChart', [data], options);
在 jQuery ready()
函数中,添加清单 10-20 中的函数。这是一个 jQuery 函数,其中将jqplotDataClick
事件与函数的执行绑定在一起。作为参数,该事件接受 jqPlot 对象的一些属性值,如seriesIndex
、pointIndex
和data
。这些值将被转换成一个字符串,并用 jQuery html()
函数连接起来。这个 HTML 文本将被发送到网页中的info1
元素。
清单 10-20。ch10_12.html
$('#myChart').bind('jqplotDataClick',
function (ev, seriesIndex, pointIndex, data) {
$('#info1').html('series: ' + seriesIndex +
', point: '+pointIndex+', data: '+data);
}
);
现在,您在想要显示带有值的文本的地方添加一个<span>
元素。这个元素将显示一个“还没有”的消息,直到你点击一个酒吧。然后,根据所点击的点,新文本将用值替换消息:
<div><span>You clicked: </span><span id="info1">Nothing yet</span></div>
图 10-16 显示了当用户点击“France”栏时触发的事件对应的消息。
图 10-16。
By clicking a bar, you can obtain its values
jqplotRightClick 事件
这个例子涵盖了 jqPlot 提供的另一个事件:jqplotRightClick
。这个事件需要在options
中显式激活(见清单 10-21)。这导致 jqPlot 在用户右键单击一个栏时触发一个jqplotRightClick
事件。
清单 10-21。ch10_13.html
var options = {
title: 'Foreign Customers',
captureRightClick: true,
series:[{renderer: $.jqplot.BarRenderer}],
axes: {
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer
}
}
};
接下来,您需要用清单 10-22 中的函数替换前面的 jqPlot 函数。
清单 10-22。ch10_13.html
$('#myChart').bind('jqplotDataRightClick',
function (ev, seriesIndex, pointIndex, data) {
$('#info1').html('series: ' + seriesIndex +
', point: '+pointIndex+', data: '+data);
}
);
一般效果是一样的,只是这次你要右击而不是左键。右键单击“西班牙”栏会得到图 10-17 中的结果。
图 10-17。
By right-clicking a bar, you obtain its values
其他条形图事件
通常,您可能想要捕获另一个事件:当鼠标停留在一个条上时,jqPlot 触发一个jqplotDataMouseOver
事件。当您显式禁用突出显示时,也会生成此事件。当用户将鼠标放在栏上时,该事件将持续触发。相比之下,另一个事件jqplotDataHighlight
只触发一次,即当用户第一次将鼠标放在工具栏上时。当用户离开酒吧时,jqPlot 触发第三个事件:jqplotDataUnhighlight
。只有启用突出显示时,才会生成后两个事件。
继续上一个例子(见清单 10-22),你用另外两个函数替换捕获jqplotDataClick
事件的 jQuery 函数(见清单 10-23)。第一个函数会在您将鼠标放在一个栏上时,立即向info1
元素发送一个具有相同值的 HTML 文本。第二个将在您离开工具栏时用'Nothing'
替换info1
元素中的前一个字符串。
清单 10-23。ch10_14a.html
$('#myChart').bind('jqplotDataHighlight',
function (ev, seriesIndex, pointIndex, data) {
$('#info1').html('series: ' + seriesIndex +
', point: '+pointIndex+', data: '+data);
}
);
$('#myChart').bind('jqplotDataUnhighlight',
function (ev) {
$('#info1').html('Nothing');
}
);
现在,您希望看到jqplotDataMouseOver
事件和jqPlotDataHighlight
事件之间的行为差异。要理解这种差异,最好的方法就是用一个例子来比较它们。这一次,您将计算当鼠标悬停在一个条上时触发的事件数。为此,定义一个计数器nEvents
,将其初始化为 0。正如预期的那样,使用jqPlotDataHighlight
事件,每当您将鼠标放在一个条上时,计数器被设置为 1,当您移出该条时,计数器的值为 0。对于jqplotDataMouseOver
事件,行为非常不同:计数器持续增加,将光标保持在同一个条上。在这两种情况下,您都使用jqplotDataUnhighlight
事件在每次离开酒吧时重置计数器。
首先,您需要更改info1
HTML 元素:
<div><span>Events: </span><span id="info1">Nothing yet</span></div>
然后,为了研究jqplotDataHighlight
事件的行为,用清单 10-24 中的两个函数替换这两个 jQuery 函数。
清单 10-24。ch10_14b.html
nEvents = 0;
$('#myChart').bind('jqplotDataHighlight',
function (ev, seriesIndex, pointIndex, data) {
nEvents = nEvents + 1;
$('#info1').html(nEvents);
}
);
$('#myChart').bind('jqplotDataUnhighlight',
function (ev) {
$('#info1').html('Nothing');
nEvents = 0;
}
);
如图 10-18 所示,当鼠标停留在“西班牙”栏上时,计数器给出稳定的 1。
图 10-18。
Counting how many jqPlotDataHighlight
events occur
为了研究jqplotDataMouseOver
事件的行为,您可以用清单 10-25 中的两个函数替换这两个 jQuery 函数。
清单 10-25。ch10_14c.html
nEvents = 0;
$('#myChart').bind('jqplotDataMouseOver',
function (ev, seriesIndex, pointIndex, data) {
nEvents = nEvents + 1;
$('#info1').html(nEvents);
}
);
$('#myChart').bind('jqplotDataUnhighlight',
function (ev) {
$('#info1').html('Nothing');
nEvents = 0;
}
);
如图 10-19 所示,当鼠标停留在“西班牙”栏上时,计数器不断增加数值。
图 10-19。
Counting how many jqplotDataMouseOver
events occur
点按栏以文本形式显示信息
由于 jqPlot 在管理事件方面的潜力,让我们借此机会看一个常见的案例。通过单击一个条形,您可以获得有关该条形的信息,并将其显示在 HTML 页面上的文本框中。这是通过将侦听器绑定到jqlotDataClick
事件来实现的。
在这个例子中,我们从生成水平堆积条形图的代码开始(见清单 10-26)。
清单 10-26。ch10_15.html
var data = [12, 8, 6, 10, 7];
var data2 = [14, 12, 4, 14, 11];
var data3 = [18, 10, 5, 9, 9];
var ticks = ['Germany', 'Italy', 'Spain', 'France', 'UK'];
var options = {
title: 'Foreign Customers',
stackSeries: true,
seriesDefaults:{
renderer: $.jqplot.BarRenderer,
pointLabels: { show: true, location: 's' }
},
series:[
{label: 'Electronics'},
{label: 'Software'},
{label: 'Mechanics'}
],
axes: {
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer,
ticks: ticks
}
},
legend: {
show: true,
placement: 'outsideGrid',
location: 'e'
}
};
$.jqplot ('myChart', [data, data2, data3], options);
和前面的事件示例一样,最后您添加一个 jQuery 函数来捕获jqplotDataClick
事件,并向info1
元素发送一组信息(参见清单 10-27)。
清单 10-27。ch10_15.html
$('#myChart').bind('jqplotDataClick',
function (ev, seriesIndex, pointIndex, data) {
$('#info1').html('series: ' + seriesIndex +
', point: '+pointIndex+', data: '+data);
}
);
每当您点击一个条时,该功能将刷新您放置了<span>
元素的地方显示的信息,其中'info1'
为id
。在这里,您将<span>
元素添加到 HTML 页面:
<span id="info1">Information will be provided here </span>
现在,通过单击一个条形的突出显示区域,您可以在文本框中获得与该条形相关的所有数据,如图 10-20 所示。
图 10-20。
By clicking a stacked bar, you obtain its values
处理图例
使用条形图时,您利用了图例,这是大多数图表的关键组成部分。图例是 jqPlot 中定义的元素。通常,您只需要调用options
中的legend
对象,就可以在图表旁边弹出图例。在这里,您将更详细地分析这个有用的元素。
什么时候有必要使用图例?当您处理多序列数据时,也就是说,当您有一组数据时,通常用不同的颜色来区分。图例除了在一个小空间中报告每种颜色和一个区分该组元素的标签之间存在的关系之外,什么也不做。
添加图例
前面的例子(见图 10-20 )非常适合研究传说。通过观察堆积图,您可以很容易地看到每个国家的条形图由三部分组成,这三部分用不同的颜色表示。因此,您知道图表中显示了三个系列。此外,您还知道每个系列占总价值的比例,但是仍然缺少关键信息:哪些类别由哪些颜色表示。通过添加图例,您将阐明三种颜色与这些类别之间的关联:“机械”、“软件”和“电子”
因此,继续使用前一个例子中的代码(参见清单 10-26 和 10-27),让我们将legend
定义添加到options
对象中,如清单 10-28 所示。为此,您不必包含任何插件;你只需要将show
属性设置为'true'
。
清单 10-28。ch10_15.html
var options = {
...
axes: {
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer,
ticks: ticks
}
},
legend: {
show: true,
placement: 'outsideGrid',
location: 'e'
}
};
$.jqplot ('myChart', [data,data2,data3],options);
图 10-21 显示了带有图例的图表。
图 10-21。
A stacked bar chart with a legend
placement
属性指定您想要图例的位置;省略它,您将获得默认行为:图例绘制在图表内部。为了避免覆盖条形,您可以通过设置location
属性来改变图例的位置,如清单 10-29 所示。
清单 10-29。ch10_16a.html
legend: {
show: true,
}
这样,如图 10-22 所示,图表被改变,图例被画在里面(默认),在右上角('ne'
【东北】)。
图 10-22。
The default legend position is inside the chart, in the top-right corner
对于更彻底的方法,最好使用级联样式表(CSS)定制。您将为图例使用一些 CSS 类来修改默认属性——主要是 CSS 类table.jqplot-table-legend
。
例如,您可以将规范添加到清单 10-30 提供的 CSS 类中。
清单 10-30。ch10_16b.html
<style>
table.jqplot-table-legend {
background-color: rgba(175, 175, 175, 1);
font: "Arial Narrow";
font-style: italic;
font-size: 13pt;
color: white;
}
</style>
并且,图例将会改变,如图 10-23 所示。
图 10-23。
The modified legend, using CSS styles
增强的图例
如果查看 jqPlot 发行版中的几个插件,会发现一个与 legends 相关的插件:EnhancedLegendRenderer。这个插件扩展了图例的功能:单击图例项,可以显示或隐藏相应的序列。用一个具体的例子就可以看出这一点。首先,将插件包含在您的 web 页面中:
<script type="text/javascript"
src="../src/plugins/jqplot.enhancedLegendRenderer.min.js"></script>
或者,如果您更喜欢使用 CDN 服务,您可以按如下方式操作:
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.enhancedLegendRenderer.min.js
></剧本>
然后,你必须激活options
中的插件,就像你激活其他插件一样,如清单 10-31 所示。
清单 10-31。ch10_16c.html
legend: {
renderer: $.jqplot.EnhancedLegendRenderer,
show: true,
placement: 'outsideGrid',
location: 'ne'
}
在浏览器中加载页面后,您会得到图 10-24 中的图表。如果单击图例中的某个项目,相应的系列将从图表中消失,留下一个空白。如果您只想分析系列的子集,而忽略其他部分,这将非常有用。因此,让我们单击图例中的“软件”,看看会发生什么。
图 10-24。
You can hide a series by selecting an item in the legend
在图 10-24 的图例中,属于软件系列的橙色部分已经消失,项目“软件”被划掉。
这种效果是累积的,你可以一个接一个地隐藏所有的系列。如果您再次单击删除线项目,相应的系列将再次出现在图表中。
自定义图例突出显示
您已经看到了如何使用 jqPlot 默认提供的图例。但是,您可以通过在 HTML 中实现一个简单的表来创建自定义图例,然后动态填充它,用您的系列的标签填充它。因为您必须从头开始创建自己的图例,所以首先需要选择一种样式。你可以通过使用一个外部 CSS 文件或者直接在网页中编写样式来定义 CSS 样式,如清单 10-32 所示。
清单 10-32。ch10_17.html
<style type="text/css">
table.sample {
border-width: thin;
border-spacing: 0px;
border-style: outset;
border-color: rgb(221, 221, 221);
border-collapse: collapse;
}
table.sample th {
border-width: 1px;
padding: 1px;
border-style: inset;
border-color: gray;
}
table.sample td {
border-width: 1px;
padding: 1px;
border-style: inset;
border-color: gray;
}
</style>
您已经使用了三个不同的 CSS 类。第一个指定整个表格的样式。另外两个被定义为分别为标题和单元格指定特定的样式。
在定义了样式之后,为了我们的目的,你可以使用一个简单的条形图的代码(见清单 10-33)。
清单 10-33。ch10_17.html
var data = [['Germany', 12], ['Italy', 8], ['Spain', 6], ['France', 10], ['UK', 7]];
var options = {
title: 'Foreign Customers',
series:[{renderer:$.jqplot.BarRenderer}],
axes: {
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer
}
}
};
$.jqplot ('myChart', [data], options);
下一步是将自定义图例绑定到jqplotDataHighlight
和jqplotDataUnhighlight
事件(见清单 10-34)。您已经看到了这些,以及如何使用 jQuery 方法将对象绑定到它们。在这种情况下,你会做得更多;您将确保整个定制图例是仅用几行 jQuery 动态创建的。这些将包括数据数组。与 jqPlot 提供的默认图例相比,在这里您可以获得比表示组系列成员的标签更多的内容。还可以添加汇总值(使用 JavaScript 函数)或简单的 y 值。
清单 10-34。ch10_17.html
$(document).ready(function(){
var data = ...
var options = ...
$.jqplot ('myChart', [data], options);
$.each(data, function(index, val) {
$('#legend1').append('<tr><td>'+val[0]+'</td><td>'+val[1]+'</td></tr>');
});
$('#myChart').bind('jqplotDataHighlight',
function (ev, seriesIndex, pointIndex, data) {
var color = 'rgb(100%, 90%, 50%)';
$('#legend1 tr').css('background-color', '#ffffff');
$('#legend1 tr').eq(pointIndex+1).css('background-color', color);
});
$('#myChart').bind('jqplotDataUnhighlight',
function (ev, seriesIndex, pointIndex, data) {
$('#legend1 tr').css('background-color', '#ffffff');
});
});
第一个 jQuery 函数处理数据数组的值。另外两个在鼠标经过图例项时将图例绑定到突出显示事件。此外,这些函数还将样式属性的变化绑定到这些事件,在本例中是项目的背景颜色。
现在,您需要定义两个不同的区域来插入图表和图例。您可以使用 HTML 表格来实现这一点。因此,你将清单 10-35 中的代码添加到 HTML 页面的<body>
部分。
清单 10-35。ch10_17.html
<table style="margin-left:auto; margin-right:auto;">
<tr>
<td><div id="myChart" style="width:460px; height:340px;"></div></td>
<td><div style="height:340px;">
<table id="legend1" class="sample" >
<tr><th>Nation</th><th>Customers</th></tr>
</table>
</div>
</td>
</tr>
</table>
最后,您可以加载带有新自定义图例的新页面(参见图 10-25 )。
图 10-25。
A custom HTML legend
自定义工具提示
除了图例,条形图中另一个非常常用的项目是工具提示。正如有可能使用代码创建自定义图例一样,也有可能自定义工具提示,创建非常新颖的效果。当你将鼠标放在一个条上并高亮显示它时,会显示一个工具提示,但是,与默认的 jqPlot 工具提示不同,它完全是以 HTML 格式构建的。这极大地拓展了你艺术表达的可能性,给你的星盘增添了一丝个性(使用 jqPlot,一切都有过于“标准 jqPlot”的风险)。在这个例子中,你想要展示一个小图标图像如何给一个条形图一个非常好的效果。
在开始编写代码之前,让我们创建一个目录,并将其命名为flags
(您可以随意命名)。在这个目录中,您将存储所有要使用的可移植网络图形(PNG)图像文件。这些图标是各国的国旗,您将在条形图的 x 轴上报告。从互联网上很容易找到并下载这些 PNG 文件。
Note
在工具提示中显示标志所需的 PNG 文件包含在本书随附的源代码中,您可以在本书的 press 产品页面( www.apress.com/9781430262893
)的“源代码/下载”选项卡中找到。
完成标志图像后,第一步是创建自定义工具提示。您需要将工具提示绑定到jqplotDataHighlight
和jqplotDataUnhighlight
事件。您可以用几行 jQuery 动态创建定制的工具提示。
这里,你从你已经使用过的条形图开始(见清单 10-36),因为它代表了一个简单的例子来理解开发这种定制工具提示的方法。
清单 10-36。ch10_18.html
var data = [['Germany', 12], ['Italy', 8], ['Spain', 6], ['France', 10], ['UK', 7]];
var options = {
title: 'Foreign Customers',
series:[{renderer:$.jqplot.BarRenderer}],
axes: {
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer
}
}
};
$.jqplot ('myChart', [data], options);
你必须添加另外两个数据数组,包含一些字符串(见清单 10-37)。您将在动态生成的工具提示中使用这些。
清单 10-37。ch10_18.html
var tick = ['Germany', 'Italy', 'Spain', 'France', 'UK'];
var icon = ['germany.png', 'italy.png', 'spain.png', 'france.png', 'uk.png'];
您将jqplot()
函数的返回值赋给一个变量,因为您稍后将需要使用它。
var myPlot = $.jqplot ('myChart', [data], options);
清单 10-38 给出了将事件绑定到自定义工具提示的 jQuery 代码。
清单 10-38。ch10_18.html
$('#myChart').bind('jqplotDataHighlight',
function (ev, seriesIndex, pointIndex, data) {
var chart_left = $('#myChart').offset().left;
var chart_top = $('#myChart').offset().top;
var x = data[0]*95+20;
var y = myPlot.axes.yaxis.u2p(data[1]);
var color = 'rgb(30%,50%,60%)';
$('#tooltip1').css({left:chart_left+x, top:chart_top+y});
$('#tooltip1').html('<span style="font-size:16px; font-weight:bold; color:' +
color + ';">' + tick[data[0] - 1] +
'</span><br/><img src="flags/'+ icon[data[0] - 1]+
'" width="30" height="20"><br/> n:' + data[1]);
$('#tooltip1').show();
}
);
$('#myChart').bind('jqplotDataUnhighlight',
function (ev, seriesIndex, pointIndex, data) {
$('#tooltip1').empty();
$('#tooltip1').hide();
}
);
最后,你必须在网页的<body>
部分创建两个<div>
元素,如清单 10-39 所示。在第一个元素中,jqPlot 将在画布上生成您的自定义工具提示;在第二个例子中,jqPlot 将创建绘制条形图的画布。
清单 10-39。ch10_18.html
<div id="myChart" style="height:300px; width:500px;"></div>
<div id="tooltip1" style="position:absolute; height:0px; width:0px;"></div>
图 10-26 显示了高亮显示的“德国”栏,旁边有定制的工具提示。这种情况会发生在条形图中的所有条形上,每次用户将鼠标悬停在条形上突出显示它们时,每个条形都会在工具提示中显示相应的标志。
图 10-26。
A bar chart with custom tool tips
摘要
在本章中,您已经看到了如何使用 BarRenderer 插件在条形图中表示您的数据。您开始看到,随着这个渲染器插件的引入,jqPlot 主对象的结构逐渐被新的属性和对象所丰富。通过实际的例子,您学习了如何用rendererOptions
改变属性和对象属性的值。
您还了解了使用同一组数据有时可能会获得不同的表示。知道如何选择哪种表示更适合你的需求是本书的基本目标之一。为此,您在分组条形图和堆积条形图中使用了相同的数据集。在这两种情况下,您都用垂直和水平条实现了数据表示。
在本章的后面,你看了一些例子,展示了 jqPlot 如何允许你使用特殊函数来处理事件。此外,您还进一步研究了图例组件以及使用 HTML 代码定制图例的可能性。然后,您将相同的方法应用于工具提示。
通常,需要条形图表示的数据类型也可以通过另一种类型的图表来很好地表示:饼图。在下一章中,您将发现 jqPlot 库如何处理这种类型的图表。
十一、jqPlot 饼图和圆环图
Abstract
饼图和圆环图是将数据分解成各个组成部分的绝佳方式。饼图是一个圆形的图表,分为扇形或“切片”,其主要目的是说明它们的相对比例:每个切片的弧长与它所代表的数量成比例。圆环图与饼图非常相似,但中间有一个洞,并支持多个系列的比较。在这一章中,你将会看到这两种图表。本章最后讨论了多维饼图。
饼图和圆环图是将数据分解成各个组成部分的绝佳方式。饼图是一个圆形的图表,分为扇形或“切片”,其主要目的是说明它们的相对比例:每个切片的弧长与它所代表的数量成比例。圆环图与饼图非常相似,但中间有一个洞,并支持多个系列的比较。在这一章中,你将会看到这两种图表。本章最后讨论了多维饼图。
饼图
在 jqPlot 中,默认情况下,数据被解释为折线图。如果您想在饼图中显示您的数据,您需要包括 PieRenderer 插件:
<script type="text/javascript" src="../src/plugins/jqplot.pieRenderer.min.js">
</script>
或者,如果您更喜欢使用内容交付网络(CDN)服务,您可以按如下方式操作:
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins
为了更好地理解这个插件的用法,让我们以一个人在给定时间内消耗的食物量为例。在这种情况下,饼图被证明是数据表示的最佳选择。所有吃的食物组成了整个群体,各种类型的食物就是你要比较的成分。每一种食物都将由一片用不同颜色标识的薄片来代表。每一片的大小将给出一个食物种类在一个人饮食中所占比例的精确概念。你可以从[label,amount]对值的数据数组开始,如清单 11-1 所示。
清单 11-1。ch11_01a.html
var data = [ ['Dairy', 212],['Meat', 140], ['Grains', 276],
['Fish', 131],['Vegetables', 510], ['Fruit', 325] ];
现在,您可以定义选项。正如您在清单 11-2 中看到的,您需要激活插件并将其应用于defaultSeries
对象。
清单 11-2。ch11_01a.html
var options = {
seriesDefaults: {
renderer: jQuery.jqplot.PieRenderer,
rendererOptions: {
showDataLabels: true
}
}
};
$.jqplot ('myChart', [data], options);
图 11-1 显示了一个简单的饼图,没有附加属性的说明。
图 11-1。
A simple pie chart
如图 11-1 所示,在其扇区内,饼图默认报告百分比。相反,如果您想要显示该值,如图 11-2 的右上角所示,您需要在renderOptions
中的dataLabels
属性上设置'value'
,如清单 11-3 所示。
清单 11-3。ch11_01b.html
rendererOptions: {
showDataLabels: true,
dataLabels: 'value'
}
如果你想添加一个边距来分隔饼图的各个部分,如图 11-2 的左下方所示,我们需要将sliceMargin
属性设置为 6(参见清单 11-4)。
清单 11-4。ch11_01c.html
rendererOptions: {
showDataLabels: true,
dataLabels: 'value',
sliceMargin: 6
}
此外,如果你想显示带有空切片的饼状图,如图 11-2 的右下方所示,你可以将fill
属性设置为'false'
并将lineWidth
属性设置为 5,以便用稍微粗一点的线条来给切片加阴影(见清单 11-5)。
清单 11-5。ch11_01d.html
rendererOptions: {
showDataLabels: true,
dataLabels: 'value',
sliceMargin: 6,
fill: false,
// stroke the slices with a little thicker line.
lineWidth: 5
}
图 11-2。
Different ways to set a pie chart
圆环图
饼图的一个主要问题是它们不能同时显示多个系列。因此,您必须决定是否用饼图单独表示每个系列,或者最好使用圆环图(也拼写为“甜甜圈”)。这种图表需要并使用与饼图相同的选项;因此,从饼图到圆环图的转换是即时的。您将通过一个简单的例子来了解这种转换的简单性。
首先,与饼图一样,您需要包含一个特定的插件来获得一个圆环图:
<script type="text/javascript" src="../src/plugins/jqplot.donutRenderer.min.js"> </script>
你必须在options
对象中做的唯一改变是在渲染器调用中用DonutRenderer
替换pieRenderer
对象,然后修改rendererOptions object
中第一个扇区的起始角度。默认情况下,图表从圆圈的左侧开始,但通常必须从顶部开始。因此,有必要将startAngle
属性设置为–90 度(见清单 11-6)。
清单 11-6。ch11_02.html
var options = {
seriesDefaults: {
// Make this a pie chart.
renderer:$.jqplot.DonutRenderer,
rendererOptions: {
showDataLabels: true,
dataLabels: 'value',
sliceMargin: 3,
startAngle: -90
}
}
};
jQuery.jqplot ('myChart', [data], options);
这样你就得到了一个甜甜圈图(见图 11-3 ,与图 11-2 右下方的饼状图非常相似。
图 11-3。
A simple donut chart
但是,我们选择使用圆环图而不是饼图,因为它允许我们同时表示多个系列,从而比较其组成部分的比例。因此,继续这个例子,你可以比较两个不同群体的人所吃的食物。清单 11-7 说明了如何添加另一个数据数组。
清单 11-7。ch11_02.html
var data2 = [
['Dairy', 185],['Meat', 166], ['Grains', 243],
['Fish', 166],['Vegetables', 499], ['Fruit', 370]
];
将第二个数组添加到data
,修改清单:
$.jqplot ('myChart', data,``data2
图 [11-4 展示了报告两个数值系列的圆环图。
图 11-4。
A multiseries donut chart
看着图 11-4 ,你可以立刻看到一些基本的东西不见了:一个图例。图例是必需的,因为插件会自动为每个扇区分配一种颜色,因此没有颜色参考,很难理解图表。因此,在您将图例的show
属性设置为'true'
之后,您可以选择图例的位置。为了确定在哪个位置放置图例,jqPlot 使用 location 属性,将与基本方向对应的值分配给该属性:'n'
(北)、's'
(南)、'e'
(东)和'w'
(西)。但是,也可以使用组合,例如'ne'
,来指示东北方向的位置。
假设您决定将图例定位在图表的右侧,那么您将'e'
分配给location
属性(参见清单 11-8)。
清单 11-8。ch11_02.html
legend: {
show:true,
location: 'e'
}
图例自动报告数据数组中包含的标签,如图 11-5 所示。
图 11-5。
A multiseries donut chart with a legend
多级饼图
多级饼图是一种现代格式,非常适合可视化用于显示层次关系的数据。这种图表提供了一个层次结构,从圆圈中心的根节点开始,您可以跟踪成员资格,因为它们逐渐移动到外部圆圈。为了更好地理解这种图表,让我们以一系列动物为例,逐步确定它们的等级群体。
作为输入数据数组,你想插入三个数组(见清单 11-9)。这将产生三个层次。在第一个数组中,插入最后一级,直到第三个数组,它代表根。
清单 11-9。ch11_03.html
var data = [ ['Cat', 1],['Dog', 1], ['Mouse', 1],['Snake', 1],
['Turtle', 1], ['Jellyfish', 1], ['Cuttlefish', 1] ];
var data2 = [ ['Mammals', 3],['Reptiles', 2], ['Mollusks', 2] ];
var data3 = [ ['Vertebrates', 5],['Invertebrates', 2] ];
要生成多级饼图,实际上需要修改一个圆环图,将内孔的直径设置为零。在这种情况下,您需要显示标签所代表的动物或动物群的名称,而不是显示数值;您必须将dataLabels
属性设置为'label'
。最后要修改的是颜色的设置。jqPlot 提供的默认颜色是不够的,有必要为层次结构的每个级别定义一组颜色。优选地,将相似的颜色分配给属于同一组的动物,并且对于层级的连续级别也是如此。在清单 11-10 中,特别注意了分配给每个系列的颜色序列(层次级别)。
清单 11-10。ch11_03.html
var options = {
seriesDefaults: {
renderer:$.jqplot.DonutRenderer,
rendererOptions: {
showDataLabels: true,
dataLabels: 'label',
startAngle: -90,
innerDiameter: 0,
ringMargin: 2,
shadow: false
}
},
series: [
{
seriesColors: ['#4bb2c5', '#4baacc', '#4b88aa', '#bbb2c5',
'#bbaa99', '#c5dd99', '#dddd77']
},
{
seriesColors: ['#4bbbbb', '#ccb2c5', '#c5ff99']
},
{
seriesColors: ['#aa5555', '#a3ffaa']
}]
};
$.jqplot ('myChart', [data, data2, data3], options);
最终,你的努力会得到图 11-6 中的多级饼图的回报。
图 11-6。
A multilevel pie chart
摘要
在本章中,您已经了解了 jqPlot 库如何允许您通过饼图(具有单个数据系列)或圆环图(具有多个数据系列)来表示数据,同时还可以快速了解一些主要属性以及如何在选项中设置它们。在本章的最后一部分,您创建了一个多级饼图:这是一个经典的例子,说明了如何通过适当地修改某些属性来生成一种不属于库所建议的标准图表的图表类型。
在下一章中,您将看到 jqPlot 库如何让您实现蜡烛图,以及如何处理特定的数据格式开盘-盘高-盘低-收盘(OHLC),这是这种图表的基础。
十二、jqPlot 蜡烛图
Abstract
蜡烛图广泛用于分析一段时间内的货币或价格变动。该图表由一系列竖线组成,称为烛台。它们显示了给定时间内的开盘价、收盘价、最低价和最高价(见图 12-1)。因此,这种图表通常被称为 OHLC 图表(当它报告开盘-盘高-盘低-收盘值时)或 HLC 图表(当它只报告盘高-盘低-收盘值时)。
蜡烛图广泛用于分析一段时间内的货币或价格变动。该图表由一系列竖线组成,称为烛台。它们显示给定时间段内的开盘价、收盘价、最低价和最高价(见图 12-1 )。因此,这种图表通常被称为 OHLC 图表(当它报告开盘-盘高-盘低-收盘值时)或 HLC 图表(当它只报告盘高-盘低-收盘值时)。
图 12-1。
Different ways to represent OHLC data: (a) line, (b) real body
烛台可能被描绘成简单的线条或末端有线条(称为灯芯或阴影)的盒子(称为实体)。每个烛台的高度表明了一个特定时期的价格范围。在箱形表示中,真实的主体是开盘价和收盘价之间的区域。但是,如果烛台用简单的垂直线表示,两个小的水平刻度表示开盘价(向左刻度)和收盘价(向右刻度)。此外,在蜡烛图中,根据价格是上涨还是下跌,数据图的颜色也不同。
在本章中,您将看到如何表示特定的OHLC
数据。你还将学习如何用线条或实体来格式化这样的图表。不过,首先,您需要包含 OHLCRenderer 插件。
OHLC 图表
要使 jqPlot 能够绘制蜡烛图,您必须在 web 页面中包含一个特定的插件:OHLCRenderer。
您还需要包括 DateAxisRenderer 插件,因为在蜡烛图中,您通常将日期值放在 x 轴上:
<script type="text/javascript" src="../src/plugins/jqplot.dateAxisRenderer.min.js">
</script>
<script type="text/javascript" src="../src/plugins/jqplot.ohlcRenderer.min.js">
</script>
关于输入数据数组,您必须遵守特定的顺序:
['timestamp', open, max, min, close]
对于这个例子,您使用的是一组在线可用的真实数据。这些数据取自一个由免费工具 Dukascopy 生成的逗号分隔值(CSV)文件,这个工具也可以在网上获得( www.dukascopy.com
)。您选择 2012 年大约一个月的欧元兑美元汇率值。让我们将所有这些值赋给一个变量,如清单 12-1 所示。
清单 12-1。ch12_01a.html
var ohlc = [
['8/08/2012 0:00:01', 1.238485, 1.2327, 1.240245, 1.23721],
['8/09/2012 0:00:01', 1.23721, 1.22671, 1.23873, 1.229295],
['8/10/2012 0:00:01', 1.2293, 1.22417, 1.23168, 1.228975],
['8/12/2012 0:00:01', 1.229075, 1.22747, 1.22921, 1.22747],
['8/13/2012 0:00:01', 1.227505, 1.22608, 1.23737, 1.23262],
['8/14/2012 0:00:01', 1.23262, 1.23167, 1.238555, 1.232385],
['8/15/2012 0:00:01', 1.232385, 1.22641, 1.234355, 1.228865],
['8/16/2012 0:00:01', 1.22887, 1.225625, 1.237305, 1.23573],
['8/17/2012 0:00:01', 1.23574, 1.22891, 1.23824, 1.2333],
['8/19/2012 0:00:01', 1.23522, 1.23291, 1.235275, 1.23323],
['8/20/2012 0:00:01', 1.233215, 1.22954, 1.236885, 1.2351],
['8/21/2012 0:00:01', 1.23513, 1.23465, 1.248785, 1.247655],
['8/22/2012 0:00:01', 1.247655, 1.24315, 1.254415, 1.25338],
['8/23/2012 0:00:01', 1.25339, 1.252465, 1.258965, 1.255995],
['8/24/2012 0:00:01', 1.255995, 1.248175, 1.256665, 1.2512],
['8/26/2012 0:00:01', 1.25133, 1.25042, 1.252415, 1.25054],
['8/27/2012 0:00:01', 1.25058, 1.249025, 1.25356, 1.25012],
['8/28/2012 0:00:01', 1.250115, 1.24656, 1.257695, 1.2571],
['8/29/2012 0:00:01', 1.25709, 1.251895, 1.25736, 1.253065],
['8/30/2012 0:00:01', 1.253075, 1.248785, 1.25639, 1.25097],
['8/31/2012 0:00:01', 1.25096, 1.249375, 1.263785, 1.25795],
['9/02/2012 0:00:01', 1.257195, 1.256845, 1.258705, 1.257355],
['9/03/2012 0:00:01', 1.25734, 1.25604, 1.261095, 1.258635],
['9/04/2012 0:00:01', 1.25865, 1.25264, 1.262795, 1.25339],
['9/05/2012 0:00:01', 1.2534, 1.250195, 1.26245, 1.26005],
['9/06/2012 0:00:01', 1.26006, 1.256165, 1.26513, 1.26309],
['9/07/2012 0:00:01', 1.26309, 1.262655, 1.281765, 1.281625],
['9/09/2012 0:00:01', 1.28096, 1.27915, 1.281295, 1.279565],
['9/10/2012 0:00:01', 1.27957, 1.27552, 1.28036, 1.27617],
['9/11/2012 0:00:01', 1.27617, 1.2759, 1.28712, 1.28515],
['9/12/2012 0:00:01', 1.28516, 1.281625, 1.29368, 1.290235] ];
在options
中,通过调用series
对象来激活 OHLCRenderer 插件。因为需要处理 x 轴上的日期值,所以必须激活xaxis
对象中的dateAxisRenderer
对象。使用这种类型的图表,最好定义您想要表示的时间段,而不考虑输入数据,以便更精确地控制显示的内容。为此,在xaxis
对象中指定min
和max
属性。您还可以看到,使用dateAxisRenderer
,您可以选择节拍间隔,使用文字表达式('1 day'
、'n days'
、'1 week'
、'n weeks'
、'1 month'
、'n months'
,其中n
是大于 1 的任何整数)。此外,请注意yaxis
尚未定义,或者说 y 值已归属于y2axis
。这样做是为了让 y 轴位于图表的右边,而不是默认的左边(见清单 12-2)。
清单 12-2。ch12_01a.html
var options = {
title: 'EUR-USD Exchange',
seriesDefaults:{ yaxis: 'y2axis'},
axes: {
xaxis: {
renderer: $.jqplot.DateAxisRenderer,
tickOptions: {formatString: '%b %e'},
min: "08-07-2012 16:00",
max: "09-12-2012 16:00",
tickInterval: "1 weeks"
},
y2axis: {
tickOptions:{ formatString: '$%.2f'}
}
},
series: [{ renderer: $.jqplot.OHLCRenderer}]
};
$.jqplot('myChart', [ohlc], options);
你现在有了如图 12-2 所示的 OHLC 圆图。
图 12-2。
An OHLC chart with lines
只要有整数,就像在输入数据数组中输入一样表示它们。但是,这并不总是可能的。通常,您必须处理小数点后有许多位数并且长度不同的数字。因此,有必要将这些数字标准化,只报告有效数字。您可以通过设置formatString
属性来实现这一点。这个特例需要一个带两个小数点的浮点值:'%.2f'
。
使用真实的身体和阴影
您刚刚看到的烛台图表是用棒线格式化的。如果你想要一个有真实物体和阴影的盒子,你需要设置一个额外的属性:candlestick
(见清单 12-3)。
清单 12-3。ch12_01b.html
series: [{
renderer: $.jqplot.OHLCRenderer,
rendererOptions:{ candleStick: true }
}]
现在,让我们看看代替小节线上水平刻度的真实物体。在图 12-3 中,白色方框表示价格上涨时(开盘价低于收盘价),黑色方框表示价格下跌时(收盘价低于开盘价)。
图 12-3。
An OHLC chart with boxes
对比烛台
偶尔,你需要在特定时间比较代表不同类别的烛台。在这种情况下,x 轴上没有日期,而是主题本身的名称。输入数据数组将是不同的;您必须使用 OHLC 数据所属类别的标签来分隔这些数据。对于每个实体,插入一个包含五个值的数组:
[n, open, max, min, close]
这里,n
不是时间戳,而是对应于刻度数组的索引的整数。因此,您在data1
数组中定义这些 OHLC 值,如清单 12-4 所示。在ticks
数组中,使用四个不同的标签来表示四个 OHLC 值中的每一个。
清单 12-4。ch12_02.html
var data1 = [[1, 75, 80, 40, 55], [2, 30, 60, 15, 50],
[3, 64, 75, 48, 50], [4, 67, 78, 20, 36]];
var ticks = ['Apple', 'Ubuntu', 'Microsoft', 'Android'];
将对 DateAxisRenderer 插件的调用替换为对 CategoryAxisRenderer 插件的调用:
<script type="text/javascript" src="../src/plugins/jqplot.categoryAxisRenderer.min.js">
</script>
<script type="text/javascript" src="../src/plugins/jqplot.ohlcRenderer.min.js">
</script>
如清单 12-5 所示,options
中的设置非常简单。首先,您必须用renderer
属性中的$.jqplot.CategoryAxisRenderer
替换$.jqplot.DateAxisRenderer
。此外,将ticks
数组分配给xaxis
中的ticks
对象。
清单 12-5。ch12_02.html
var options = {
axes: {
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer,
ticks: ticks
},
},
series: [{
renderer: $.jqplot.OHLCRenderer,
rendererOptions:{ candleStick: true}
}]
};
$.jqplot ('myChart', [data1], options);
这张图表完美地描绘了一个真实身体的盒子;价格下跌时该框被填充,价格上涨时该框被清空(见图 12-4 )。
图 12-4。
A comparative candlestick chart
摘要
在本章中,您已经看到了如何通过蜡烛图来表示特定的 OHLC 数据。您还学习了如何用线条或实体来格式化此类图表。
在下一章,我将讨论一整类图表,它们有一个共同的特点:它们的目的是表示数据的分布。通过这次探索,您将发现如何用 jqPlot 库实现散点图、气泡图和块图。
十三、jqPlot 散点图和气泡图
Abstract
在这一章中,我将讨论一类在表示数据分布时特别有用的图表。您可能会经常发现自己对一组数据如何沿两个不同参数定义的空间分布感兴趣,这两个参数沿 x 轴和 y 轴显示。这种数据分布可以暗示相关性或聚类。
在这一章中,我将讨论一类在表示数据分布时特别有用的图表。您可能会经常发现自己对一组数据如何沿两个不同参数定义的空间分布感兴趣,这两个参数沿 x 轴和 y 轴显示。这种数据分布可以暗示相关性或聚类。
散点图是显示数据分布的最佳选择,尤其是当需要分析大量数据时。因此,您将首先使用一个简单的示例来学习如何实现这种图表。随后,您将看到,一旦定义了两个不同的数据组(聚类),就可以通过趋势线突出显示 x 和 y 变量之间的相关性。
最后,您将分析另外两种类型的图表:气泡图和块状图。这些可以被认为是散点图的变体,其中数据点被气泡或方块取代。当您需要用三个不同的参数来表示数据时,可以使用气泡图(散点图只适用于两个参数);第三个参数由气泡的半径表示。方块图是一种特殊的散点图,在其中,您使用包含标签的方框来代替数据点。
散点图(xy 散点图)
乍一看,您可能认为散点图(也称为散点图或 xy 图)是一种点不相连的折线图,但这将是一个错误。事实上,散点图、气泡图和块状图都是一种特殊的图表。在散点图中,点由(x,y)对表示,但是您可以获得许多具有相同 x 值的点,这使得用线将它们连接起来既困难又不必要。折线图的目的是跟踪 x 值范围内 y 值的进度。散点图的目的是显示一组点,这些点可能有也可能没有某种关系(可以是非线性的)。此外,您可能希望分析这些点及其在(x,y)空间中的分布,例如,当它们分布在空间上独立的组中时。
您使用默认设置(如在折线图中),禁用点之间的线。例如,让我们看两个(x,y)数据集合,它们可能呈现某种形式的关系,如清单 13-1 所示。
清单 13-1。ch13_04a.html
var data = [[400, 35], [402, 37], [650, 55], [653, 56], [650, 50],
[700, 55], [600, 37], [601, 43], [450, 38], [473, 37],
[480, 42], [417, 37], [510, 41], [553, 44], [570, 39],
[527, 41], [617, 41], [625, 49]];
var data2 = [[100, 40], [600, 80], [200, 50], [300, 55], [400, 60],
[500, 70], [123, 43], [110, 41], [157, 45], [160, 48],
[237, 49], [248, 55], [287, 50], [321, 59], [359, 52],
[387, 62], [466, 68], [533, 74], [344, 60], [323, 51],
[430, 65]];
与折线图不同,您输入的点没有任何顺序。如前所述,您使用默认设置,通过将showLine
属性设置为“””来禁用点之间的线(参见清单 13-2)。
清单 13-2。ch13_04a.html(使用'false
'禁用点之间的线)
var options = {
title: 'Scatter Chart',
seriesDefaults: {
showLine: false,
showMarkers: true
}
};
$.jqplot('myChart', [data, data2], options);
这样你就得到图 13-1 中的散点图,其中两组数据覆盖了图表的两个不同区域。这些点被分成定义明确的组。
图 13-1。
A scatter chart
只有当两个数据集合被表示出来时,确定它们是否遵循线性或指数趋势才有意义。在这里,您可以使用 jqPlot 的趋势线功能。因此,您包括了趋势线插件:
<script type="text/javascript" src="../src/plugins/jqplot.trendline.min.js"></script>
或者,如果您更喜欢使用内容交付网络(CDN)服务,您可以按照以下方式进行:
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.trendline.min.js
然后,激活两个系列的趋势线插件,给每条线分配不同的颜色(见清单 13-3)。
清单 13-3。ch13_04b.html
var options = {
title: 'Scatter Chart',
seriesDefaults: {
showLine: false,
showMarkers: true
}
,
series: [{
trendline: {
show: true,
color: '#0000ff',
type: 'exponential'
}
},{
trendline: {
show:true,
color: '#ff0000'
}
}]
};
结果,你得到一个散点图,有两个不同的序列,每个序列都有自己的趋势线,如图 13-2 所示。
图 13-2。
A scatter chart with trend lines
泡泡图
当您需要以三维方式显示数据时,可以使用气泡图。因此,每个实体由独立值的三元组(v1,v2,v3)表示。这些值中的两个通过绘制以(x,y)点为中心的圆盘来表示。第三个值由圆盘半径(r)表示。因此,(v1,v2,v3)三元组必须转换为(x,y,r)。三个(v1,v2,v3)值中的哪一个是半径,哪一个是 x 或 y 取决于图表设计者的技能。
与 xy 图表类似,气泡图通常用于确定所表示的数据之间的可能关系,甚至用于查看它们是否属于不同的组。这种方法在科学、医学和经济数据分析中很常见。
jqPlot 中有一个专门针对气泡图的插件:BubbleRenderer。因此,有必要在您的网页中包含这个插件:
<script type="text/javascript" src="../src/plugins/jqplot.bubbleRenderer.min.js"></script>
或者,如果您更喜欢使用 CDN 服务,您可以按如下方式操作:
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.bubbleRenderer.min.js
输入数据数组每项有四个值:
[x, y, radius, <label or object>]
前两个值代表(x,y)坐标,第三个值(注意!)与气泡半径成正比,最后一个值代表引用标签(实际上也可以传递一个对象;稍后会有更多相关内容)。清单 13-4 定义了一个包含七个欧洲国家特征值的数组:作为 x 的值,你将插入表面积;对于 y,人口;半径代表经济价值。第四个值是报告州名的标签。
清单 13-4。ch13_01a.html
var data = [[301,60,29392,"Italy"], [675,65,34205,"France"],
[506,46,30625,"Spain"], [357,81,37896,"Germany"],
[450,9,37333,"Sweden"], [30,11,37736,"Belgium"],
[132,11,27624,"Greece"]];
现在,让我们分析如何设置options
变量(见清单 13-5)。您需要激活seriesDefault
对象中的 BubbleRenderer 插件,并将bubbleGradients
属性设置为“true
”。这将使“气泡”充满颜色渐变,给人一种深度感:这样圆盘看起来就像球体一样。正如您所看到的,对于这个插件,您不需要创建一个包含气泡标签的数组,然后将它显式分配给options
中的一个对象;标签由相同的输入数据阵列自动读取。在options
中指定的设置很少且简单。
清单 13-5。ch13_01a.html(设置options
变量)
var options = {
title: 'Bubble Chart with Gradient Fills',
seriesDefaults:{
renderer: $.jqplot.BubbleRenderer,
rendererOptions: {
bubbleGradients: true
},
shadow: true
},
axes: {
xaxis: {
label: "Total area [*1000 km3]"
},
yaxis: {
label: "Population [million]"
}
}
};
$.jqplot('myChart', [data], options);
最后,通过这几行代码,你得到了如图 13-3 所示的精彩气泡图。
图 13-3。
A bubble chart
前面,我们提到了将一个对象作为输入数据数组中的第四个值进行传递的可能性。在这里,你可以详细地看到这涉及到什么。您可以同时传递一个对象,该对象允许您定义每个单独元素(气泡)的标签和颜色。使用前面的例子(见清单 13-5),你可以附加不同于默认序列的颜色。例如,假设你想强调一个国家的价值高于其他国家。你拿瑞典为例,把它指定为红色。你给其他国家分配各种深浅不同的棕色。然后将瑞典的数据移到一个新的数组中,data2
;这是为了确保“瑞典”气泡总是在前景中,并且不会被其他气泡覆盖(见清单 13-6)。
清单 13-6。ch13_02.html
var data = [[301, 60, 29392, {label: 'Italy',color:'#b39524'}],
[675, 65, 34205, {label: 'France', color:'#c39564'}],
[506, 46, 30625, {label: 'Spain',color:'#a39544'}],
[357, 81, 37896, {label: 'Germany', color:'#b39524'}],
[30, 11, 37736, {label: 'Belgium',color:'#c39544'}],
[132, 11, 27624, {label: 'Greece', color:'#a39564'}]];
var data2 = [[450, 9, 37333, {label: 'Sweden', color:'#ff2524'}]];
使用相同的options
,你只需要修改jqplot()
函数,如清单 13-7 所示。您采用这个小捷径,知道最右边的数组项是最后绘制的项,因此它会出现在前景中。
清单 13-7。ch13_02.html(修改jqplot()
功能)
$.jqplot('myChart',data,``data2
图 [13-4 给出了结果。
图 13-4。
A bubble chart with a selected state in the foreground
默认的颜色顺序很适合你,但是你决定将渐变填充改为透明效果。要做到这一点,你必须添加bubbleAlpha
属性,并给它指定所需的透明度值,如清单 13-8 所示。
清单 13-8。ch13_01b.html
seriesDefaults:{
renderer: $.jqplot.BubbleRenderer,
rendererOptions: {
bubbleGradients: true,
bubbleAlpha: 0.6
},
shadow: true
},
图 13-5 显示了带有透明效果的渐变填充气泡,提供了底层气泡的一瞥。
图 13-5。
A bubble chart with transparency
方块图
块图(也称为块图)与气泡图非常相似,但它使用的不是圆盘,而是矩形。这里,矩形的大小没有任何意义,除了为那些将标签应用于给定(x,y)对的矩形提供空间。
与气泡图一样,有必要在网页中包含 BlockRenderer 插件:
<script type="text/javascript" src="../src/plugins/jqplot.blockRenderer.min.js"></script>
或者,如果您更喜欢使用 CDN 服务,您可以按如下方式操作:
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.blockRenderer.min.js
在本例中,您将使用三组数据。输入数据数组应该具有以下格式:
[x, y, 'Label'],
现在,定义三个不同的数组,如清单 13-9 所示。
清单 13-9。ch13_03.html
var data1 = [[10, 30, 'Copper'], [100, 40, 'Gold'], [50, 50, 'Silver'],
[12, 78, 'Lead'], [44, 66, 'Brass']];
var data2 = [[68, 15, 'Maple'], [33, 22, 'Oak'],[10, 90, 'Ebony'],
[94, 30, 'Beech'],[70, 70, 'Ash']];
var data3 = [[22, 16, 'PVC'], [56, 76, 'PE'], [33, 78, 'PET'],
[27, 60, 'PC'], [70, 44, 'PU']];
在options
中,你只需要激活seriesDefault
对象中的 BlockRenderer 插件(见清单 13-10)。
清单 13-10。ch13_03.html(激活BlockRenderer
插件)
var options = {
seriesDefaults:{
renderer: $.jqplot.BlockRenderer
}
};
$.jqplot ('myChart', [data1, data2, data3], options);
图 13-6 给出了您刚刚定义的框图,其中每个系列用不同的颜色标记。
图 13-6。
A block chart
摘要
在这一章中,你已经学会了如何表示一个分布。您可能会经常发现自己对研究数据在空间中的分布感兴趣,以便发现任何可能的趋势或聚类。根据您更想突出什么,您可以选择通过散点图、气泡图或方块图来表示数据。此外,您已经看到了如何突出一个分布的趋势。
在下一章中,我将收集其他类型的图表,这些图表您还没有看过,但是属于 jqPlot 库的标准类型。首先,您将学习漏斗图以及如何通过options
设置其属性。然后,您将发现贝塞尔曲线——它们是什么,以及 jqPlot 如何实现它们。
十四、jqPlot 漏斗图
Abstract
漏斗图用于显示数据从一个级别到下一个级别的逐渐减少。图表由一个倒金字塔或漏斗组成,分为不同的层次。每个级别都有自己的面积,与给定的百分比值成比例。漏斗图与饼图相似,都是将一个整体分成几个组成部分来表达。但是,漏斗图指定了级别,它们以非常精确的顺序一个接一个。这个序列可以表示层次顺序、过程的步骤等等。饼图不能做到这一点。
漏斗图用于显示数据从一个级别到下一个级别的逐渐减少。图表由一个倒金字塔或漏斗组成,分为不同的层次。每个级别都有自己的面积,与给定的百分比值成比例。漏斗图与饼图相似,都是将一个整体分成几个组成部分来表达。但是,漏斗图指定了级别,它们以非常精确的顺序一个接一个。这个序列可以表示层次顺序、过程的步骤等等。饼图不能做到这一点。
创建漏斗图
即使对于这个专门的图表,jqPlot 也提供了一个特定的插件:FunnelRenderer。因此,您需要包含它:
<script type="text/javascript" src="../src/plugins/jqplot.funnelRenderer.min.js"></script>
或者,如果您更喜欢使用内容交付网络(CDN)服务,您可以按如下方式操作:
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.funnelRenderer.min.js
使用 jqPlot,您必须注意特定于该渲染器插件的行为:FunnelRenderer 以降序对数据进行重新排序。最大值显示在漏斗的顶部,较小值显示在下方。每个漏斗部分的面积对应于其数据点的值,相对于所有值的总和(百分比)。使用此渲染器,您需要对输入数据数组使用以下格式:
['label',value]
因此,在本例中,您将数据数组定义如下:
var data = [['Sony', 1], ['Samsung', 13], ['LG', 14], ['Philips', 5]];
对于options
,您必须激活seriesDefaults
对象中的漏斗渲染器插件,并且,可选地,您可以添加一个报告系列标签的图例,如清单 14-1 所示。
清单 14-1。ch14_01a.html
var options = {
seriesDefaults: {
renderer: $.jqplot.FunnelRenderer
},
title: {
text: 'Basic Funnel Chart'
},
legend: {
location: 'e',
show: true
}
};
$.jqplot('myChart', [data], options);
图 14-1 是基本的漏斗图。
图 14-1。
A simple funnel chart
如您所见,项目的顺序已经改变,具有最高值的元素在顶部,依此类推,直到具有最低值的项目。这是基本的图表,但是您可以丰富它,例如,通过添加报告百分比的标签。为此,您必须添加dataLabel
属性,将其设置为“percent
,然后使用设置为“true
”的showDataLabel
启用它(参见清单 14-2)。
清单 14-2。ch14_01b.html
seriesDefaults: {
renderer: $.jqplot.FunnelRenderer,
rendererOptions: {
dataLabels: 'percent',
showDataLabels: true
}
},
正如您在图 14-2 中看到的,百分比值现在在漏斗图的相应部分中报告。
图 14-2。
A funnel chart with a legend and percentages
您可以做进一步的更改。例如,假设你想减小漏斗截面之间的间距,如图 14-3 所示。您可以通过传递给sectionMargin
属性的值来实现这一点。通过将值 0 赋给sectionMargin
属性,你完全消除了部分之间的空间(见清单 14-3)。
清单 14-3。ch14_01c.html
rendererOptions: {
dataLabels: 'percent',
showDataLabels: true
,
sectionMargin: 0
}
图 14-3。
A funnel chart without spaces between sections
或者,你可能想将不同的扇区表示为未填充的,并增加其边界线的宽度,如图 14-4 所示。为此,需要使用两个属性:fill
和lineWidth
。首先,您将fill
属性设置为“false
”,这将导致 jqPlot 绘制带有空白区域的截面;然后,将lineWidth
属性设置为 4,从而增加部分边缘的厚度,使它们更加可见(见清单 14-4)。
清单 14-4。ch14_01d.html
rendererOptions: {
dataLabels: 'percent',
showDataLabels: true,
fill: false,
lineWidth: 4
}
图 14-4。
A funnel chart with no filled sections
摘要
在这一章中,你学习了如何制作某些类型的漏斗图,以及如何通过options
改变它们的属性。
在下一章中,我将讨论我在前面章节中提到过的一个主题:控件。我将描述在图表中引入控件的重要性,理解每个控件背后都隐藏着一个属性options
。这为用户提供了实时选择属性的机会。
十五、将控件添加到图表
Abstract
有时,在运行时直接从浏览器更改设置,然后用这些新设置重新绘制图表会很有用。一种典型的方法是添加活动控件。这些控件使图表具有交互性,允许用户实时做出选择,例如决定图表应该如何表示。通过插入控件,用户可以控制图表的属性值,这通常需要在options
中设置。
有时,在运行时直接从浏览器更改设置,然后用这些新设置重新绘制图表会很有用。一种典型的方法是添加活动控件。这些控件使图表具有交互性,允许用户实时做出选择,例如决定图表应该如何表示。通过插入控件,用户可以控制图表的属性值,这通常需要在options
中设置。
在这一章中,你将会看到在你的网页中引入控件。您还将考虑导致选择一种控制类型而不是另一种控制类型的因素。以三个最常用的控件为特色的一系列示例将带您更深入地了解这个主题。
添加控件
对控件进行分组的一种方法是根据它们的功能。一些控件(例如,按钮、菜单)作为开关(命令控件)工作,用户可以用其触发特定事件或启动命令。其他控件(例如,复选框、单选按钮、组合框、滑块)绑定到特定的值或属性。使用这种类型的控件,用户可以通过文本字段(文本区域)进行选择或输入值。还有一些控件(例如滚动条)具有导航功能,尤其适用于需要移动对象的情况,例如列表中的选定项或包含在框架或网页中的大图像。
在这里,您将研究那些与值相关联的控件,这些控件允许用户通过选择与图表进行交互。这些控件应该以某种方式图形化地表示特定属性可以采用的值(通常分配给options
对象中的属性的相同值,仅限于您希望提供给用户的值)。您对控件的选择将取决于要设置的属性及其可能采用的值:
图 15-1。
Three of the most commonly used controls: (a) radio buttons, (b) check boxes, (c) sliders
- 为了使用户能够从一组值(例如,三种可能的颜色之一)中进行单一选择,选择互斥的单选按钮作为控件是最佳的(参见图 15-1a )。
- 为了让用户选择哪个系列应该在图表中可见,你需要使用复选框(见图 15-1b )。
- 为了允许用户在特定属性的值范围内进行选择(例如,通过调整定义颜色的红绿蓝(RGB)值来改变对象的颜色),滑块通常是最佳选择(参见图 15-1c )(在这种情况下,您将使用三个滑块作为控件,分别对应于红色、绿色和蓝色)。
可能的控制列表并没有到此为止。但是,对这些控件背后的机制的理解使图表开发人员能够处理绝大多数情况,包括最复杂的情况。
在下面的示例中,您将发现如何将这三个控件应用于您的图表。
使用单选按钮
为了说明控件的用法,让我们先来看看单选按钮。单选按钮是以列表形式分组的一组小按钮(参见图 15-1a )。它们通常被表示为小而空的圆圈,旁边有文字。如前所述,这种类型的控制与某个值或属性相关联。单选按钮的特殊性在于它们的值是互斥的;因此,用户只能选择其中之一。
举例来说,让我们以一个简单的多系列折线图为例,在该图中,不是显示所有的系列,而是让用户决定显示哪个系列。要进行选择,用户将单击其中一个单选按钮,用圆点填充圆圈。对应于该控件的系列将被绘制在图表上。
添加单选按钮控件
首先,你需要编写 HTML 页面,导入所有必要的库(见清单 11-1)。
清单 15-1。ch15_01.html
<HTML>
<HEAD>
<TITLE>Selection series with controls</TITLE>
<!--[if lt IE 9]>
<script type="text/javascript" src="../src/excanvas.js"></script>
<![endif]-->
<script type="text/javascript" src="../src/jquery.min.js"></script>
<script type="text/javascript" src="../src/jquery.jqplot.min.js"></script>
<link rel="stylesheet" type="text/css" href="../src/jquery.jqplot.min.css" />
<script>
$(document).ready(function(){
//add your code here
});
</script>
</HEAD>
<BODY>
<div id="myChart" style="height: 300px; width: 500px;"></div>
<!-- add the table with the controls here -->
</BODY>
</HTML>
或者,如果您更喜欢使用内容交付网络(CDN)服务,您可以使用以下代码:
<!--[if lt IE 9]>
<script src="
http://cdn.jsdelivr.net/excanvas/r3/excanvas.js
<![endif]-->
<script src="
http://code.jquery.com/jquery-1.9.1.min.js
<script type="text/javascript"
src="
http://cdn.jsdelivr.net/jqplot/1.0.8/jquery.jqplot.min.js
<link rel="stylesheet" type="text/css"
href="
http://cdn.jsdelivr.net/jqplot/1.0.8/jquery.jqplot.min.css
您从一个折线图开始,在折线图中,您将表示四组值。每个序列中的每个元素将由一对(x,y)值表示;您将这四个序列的值插入到 jQuery $(document).ready()
函数中定义的数据集中,如清单 15-2 所示。
清单 15-2。ch15_01.html
var dataSet = {
data1: [[1, 1], [2, 2], [3, 3], [4, 2], [5, 3], [6, 4]],
data2: [[1, 3], [2, 4], [3, 5], [4, 6], [5, 5], [6, 7]],
data3: [[1, 5], [2, 6], [3, 8], [4, 9], [5, 7], [6, 9]],
data4: [[1, 7], [2, 8], [3, 9], [4, 11], [5, 10], [6, 11]]
};
但是,不是像前面看到的那样用不同颜色的线条显示所有四个系列,而是让用户一次只显示一个系列。一旦图表加载到浏览器中,用户将能够选择四个系列中的任何一个并在它们之间切换,而不必加载新的页面。
首先,只表示第一个序列(data1
)(见清单 15-3)。
清单 15-3。ch15_01.html
var options = {
seriesDefaults: {
showMarker: false
},
title: 'Series selection',
axes: {
xaxis: {},
yaxis: {
min: 0,
max: 12
}
}
};
var plot1 = $.jqplot('myChart', [dataSet.data1], options);
Note
在这个例子中,您将由$.jqplot()
函数返回的值存储在plot1
变量中。这允许您访问jqplot
对象的内容,更改值,并调用它的方法,包括replot()
函数,它允许您再次绘制图表,包括新的更改。
用户将从一组可能的选项中选择一个选项;单选按钮是实现这一目的的最佳选择。因此,让我们为每个单选按钮分配一个系列。正如你在清单 15-4 中看到的,所有的控件(按钮)都包含在一个表格的内部列表中。每个按钮由一个<input>
元素指定,其中四个系列也被指定为值。
清单 15-4。ch15_01.html
<table>
<tr>
<td>
<div>
<ul>
<li><input name="dataSeries" value="data1" type="radio" checked />First Series</li>
<li><input name="dataSeries" value="data2" type="radio" />Second Series</li>
<li><input name="dataSeries" value="data3" type="radio" />Third Series</li>
<li><input name="dataSeries" value="data4" type="radio" />Fourth Series</li>
</ul>
</div> </td>
</tr>
</table>
然而,在 HTML 页面中设置控件定义是不够的;您还必须创建将单选按钮与 jqPlot 图表相关联的函数。根据哪个单选按钮处于checked
状态,图表中将加载不同于数据集的集合。
在选择不同的单选按钮时,用户将选中的属性从'false'
更改为'true'
。单选按钮的状态改变涉及到change()
功能的激活,该功能检测到该事件。该函数将数据集中的一个新集合分配给plot1
变量(包含关于 jqPlot 图表的所有信息),并最终强制重新绘制图表。这样,新数据就显示在图表中,而不必重新加载页面(见清单 15-5)。
清单 15-5。ch15_01.html
$(document).ready(function(){
...
var plot1 = $.jqplot ('myChart', [dataSet.data1], options);
$("input[type=radio][name=dataSeries]").attr("checked", false);
$("input[type=radio][name=dataSeries][value=data1]").attr("checked", true);
$("input[type=radio][name=dataSeries]").change(function(){
var val = $(this).val();
plot1.series[0].data = dataSet[val];
plot1.replot();
});
});
要定制控件表中的元素,可以添加一点层叠样式表(CSS)样式,如清单 15-6 所示。
清单 15-6。ch15_01.html
<style>
li {
font-family: "Verdana";
font-size: 16px;
font-weight: bold;
text-shadow: 1px 2px 2px #555555;
margin: 3px;
list-style: none;
}
</style>
如果您在浏览器中加载此网页,您将获得图 15-2 中的图表。
图 15-2。
With radio buttons it is possible to select only one series of data
现在,用户可以选择在图表中显示哪个系列。选择单选按钮作为控件后,图表一次将只显示一组数据。
绘制图表后访问属性
到目前为止,您已经使用了options
对象来定义图表的属性值(通过更改默认值),然后将其作为参数传递给$.jqplot()
函数。但是,这仅适用于您希望在绘制图表之前描述其特征的情况。如果随后需要访问属性值,该怎么办?
事实上,通过引入控件作为参数,您也引入了在绘制图表后更改这些属性的可能性。因此,必须有一种方法来访问这些值,编辑它们,然后运行命令来重绘图表(就像使用replot()
函数时一样)(见清单 15-5)。
您已经看到,您可以接收整个jqplot
对象作为由$.jqplot()
函数返回的值,并将它存储在一个变量中(在前面的例子中,是plot1
变量),以便您可以在以后访问它的内容。
一个jqplot
对象实际上包含了定义整个 jqPlot 库的所有对象——它们的属性和方法,并且每个特定的实例(例如plot1
)都在特定图表的表示中实现。
因此,当您编写 JavaScript 代码来定义处理特定事件(如用户使用控件)的函数时,您可以访问这些值,并在页面设计完图表后更改它们,然后运行命令以所需的更改重新绘制图表。这增加了图表中所需的交互性。
继续前面的例子(参见清单 15-1 到 15-6),您会注意到这些线条都是用蓝色绘制的。现在让我们做一些更改,这样这次用户可以选择绘制系列的颜色。
要做到这一点,你要在表格中添加另一组控件:第二列单选按钮,每一列代表一种颜色(见清单 15-7)。
清单 15-7。ch15_02.html
<table>
<tr>
<td>
<div>
<ul>
<li><input name="dataSeries" value="data1" type="radio" checked />First series</li>
<li><input name="dataSeries" value="data2" type="radio" />Second series</li>
<li><input name="dataSeries" value="data3" type="radio" />Third series</li>
<li><input name="dataSeries" value="data4" type="radio" />Fourth series</li>
</ul>
</div>
</td>
<td>
<div>
<ul>
<li><input name="colors" value="#4bb2c5" type="radio" checked />Blue</li>
<li><input name="colors" value="#ff3333" type="radio" />Red</li>
<li><input name="colors" value="#44bb44" type="radio" />Green</li>
<li><input name="colors" value="#ffaa22" type="radio" />Orange</li>
</ul>
</div>
</td>
</tr>
</table>
接下来,将清单 15-8 中用粗体突出显示的行添加到 JavaScript 代码中。
清单 15-8。ch15_02.html
$("input[type=radio][name=dataSeries]").attr("checked", false);
$("input[type=radio][name=dataSeries][value=data1]").attr("checked", true);
$("input[type=radio][name=dataSeries]").change(function(){
var val = $(this).val();
plot1.series[0].data = dataSet[val];
plot1.series[0].renderer.shapeRenderer.strokeStyle = col;
plot1.replot();
});
var col = "#4bb2c5";
$("input[type=radio][name=colors]").change(function(){
col = $(this).val();
});
图 15-3 展示了用户如何在四种不同的颜色中进行选择来决定要表现的系列。这是一个添加控件如何增加用户和图表之间的交互性的例子。
图 15-3。
The user can select a different combination of colors and series
使用滑块
在前面的示例中,用户首先通过选中第二列中的一个单选按钮来设置颜色,然后从第一列中选择要用该颜色表示的系列。因此,这一过程包括在两个不同的时间做出的两个选择。这一次,您将保持第一列不变,用户从中选择要显示的系列(互斥),但是在单选按钮列的位置,您将插入一组三个滑块。在这种情况下,用户选择要显示的系列,一旦以预定义的颜色绘制在图表上,他或她就可以通过调整组成它的三个 RGB 值来修改该颜色。现在,你有一个选择,然后进行微调。
当需要通过滚动给定范围内的连续值来更改属性值时,滑块就是所需的控件。在这种情况下,需要三个滑块,每种颜色(红、绿、蓝)一个,以便用户可以调整 RGB 值来获得所需的颜色。
使用前面的例子(参见清单 15-7 和 15-8),首先选择 jQuery 接口库(jQuery UI)来获得滑块(关于如何使用 jQuery UI 小部件实现滑块的详细信息,参见第二章)。因此,在将滑块添加到网页之前,必须导入属于此库的所有必要文件:
<link rel="stylesheet"
href="
http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css
<script src="
http://code.jquery.com/ui/1.10.3/jquery-ui.min.js
Note
如果您在随本书提供的源代码可用的工作空间中工作(参见附录 A),您可以通过使用以下参考来访问工作空间中已经包含的库:
<link rel="stylesheet" href="../src/css/smoothness/jquery-ui-1.10.3.custom.min.css" />
<script type="text/javascript" src="../src/js/jquery-ui-1.10.3.custom.min.js"></script>
一旦导入了所有文件,就可以开始在 HTML 表格中插入三个滑块了。正如您在清单 15-9 中看到的,您删除了包含单选按钮的第二列,代之以一组<div>
元素(如果您直接从这里开始,您可以复制整个清单,而不仅仅是粗体文本)。jQuery UI 会将它们转换成滑块(参见第二章)。
清单 15-9。ch15_04.html
<table>
<tr>
<td>
<div>
<ul>
<li><input name="dataSeries" value="data1" type="radio" checked />First series</li>
<li><input name="dataSeries" value="data2" type="radio" />Second series</li>
<li><input name="dataSeries" value="data3" type="radio" />Third series</li>
<li><input name="dataSeries" value="data4" type="radio" />Fourth series</li>
</ul>
</div>
</td>
<td>
<div id="red">
<div id="slider-text">
<div id="0">0</div>
<div id="1">255</div>
</div>
</div>
<div id="green">
<div id="slider-text">
<div id="0">0</div>
<div id="1">255</div>
</div>
</div>
<div id="blue">
<div id="slider-text">
<div id="0">0</div>
<div id="1">255</div>
</div>
</div>
</td>
</tr>
</table>
此外,您还使用slider-text id
向每个滑块添加了两个数值。这些值只不过是用于显示三个滑块所覆盖的值范围(0–255)的最小值和最大值的标签。当您必须表示网页中每张幻灯片的比例时,这种方法非常有用。
现在让我们添加所有的 CSS 样式指令,以确保这些新控件可以在现有页面的上下文中正确显示(见清单 15-10)。
清单 15-10。ch15_04.html
<style>
...
#red, #green, #blue {
float: left;
margin: 15px;
left: 50px;
}
#red .ui-slider-range {
background: #ef2929;
}
#red .ui-slider-handle {
border-color: #ef2929;
}
#green .ui-slider-range {
background: #8ae234;
}
#green .ui-slider-handle {
border-color: #8ae234;
}
#blue .ui-slider-range {
background: #729fcf;
}
#blue .ui-slider-handle {
border-color: #729fcf;
}
#slider-text div {
font-family: "Verdana";
font-size: 10px;
position: relative;
left: 17px;
}
</style>
关于 JavaScript 中的代码部分,您只保留管理用于选择所需系列的单选按钮的部分,将其与处理 RGB 值的新代码部分集成,通过三个滑块进行调整,如清单 15-11 所示。然后,这三个 RGB 值通过适当的函数转换为十六进制数,并组合形成 HTML 颜色代码,用井号(#)
表示,后跟六个十六进制字符('rrggbb'
),其中每一对代表一个从 0 到 255 的值,转换为十六进制格式。
清单 15-11。ch15_04.html
$(document).ready(function(){
...
$("input[type=radio][name=dataSeries]").attr("checked", false);
$("input[type=radio][name=dataSeries][value=data1]").attr("checked", true);
$("input[type=radio][name=dataSeries]").change(function(){
var val = $(this).val();
plot1.series[0].data = dataSets[val];
plot1.series[0].renderer.shapeRenderer.strokeStyle = "#" + col;
plot1.replot();
});
var col = "4bb2c5";
function hexFromRGB(r, g, b) {
var hex = [
r.toString( 16 ),
g.toString( 16 ),
b.toString( 16 )
];
$.each( hex, function( nr, val ) {
if ( val.length === 1 ) {
hex[ nr ] = "0" + val;
}
});
return hex.join( "" ).toUpperCase();
};
$( "#red, #green, #blue" ).slider({
orientation: "vertical",
range: "min",
max: 255,
change: refreshPlot
});
// set col to default "#4bb2c5";
$( "#red" ).slider( "value", 255 );
$( "#green" ).slider( "value", 140 );
$( "#blue" ).slider( "value", 60 );
function refreshPlot() {
var r = $( "#red" ).slider( "value" );
var g = $( "#green" ).slider( "value" );
var b = $( "#blue" ).slider( "value" );
var col = hexFromRGB(r, g, b);
plot1.series[0].renderer.shapeRenderer.strokeStyle = "#" + col;
plot1.replot();
}
$("[id=0]").css('top','90px');
$("[id=1]").css('top','-20px');
});
清单 15-11 中的最后两行代码使用 jQuery css()
函数将 CSS 样式分配给特定的 HTML 元素(参见第二章)。在所有带有id = 0
和id = 1
的元素上进行选择,也就是说,<div>
元素包含滑块刻度的标签。您可以设置 CSS top 属性,将每个刻度标签放置在相应滑块旁边的特定高度。
在图 15-4 中,用户可以通过三个滑块修改 RBG 值来决定要显示和更改的系列。
图 15-4。
A chart with three slider widgets added to adjust the RGB levels
使用复选框
在前面的例子中,用户只能从可以显示的系列中选择一个。然而,典型地,用户将希望能够决定哪些系列应该被显示,哪些不应该被显示,例如,选择同时显示两个或更多组。这需要处理同一组中的多种选择。为了让用户做出这种选择,你必须选择复选框。
通常,复选框被分组在一个列表中,由空框表示(见图 15-1 )。与单选按钮不同,这些控件不是互斥的,而是多项选择。因此,您可以选择它们所代表的全部、部分值,或者不选择任何值(而对于单选按钮,必须选择一个项目)
与单选按钮类似,每个系列都有一个复选框,如果复选框被选中,相应的系列将显示在图表中。然而,与单选按钮不同,复选框是相互独立的:它们的状态(选中或未选中)不会影响其他复选框的状态。
通常,当您有一个复选框列表时,添加两个具有“全部选中/取消选中”功能的按钮会非常有用,从而允许通过一次单击来选择/取消选择所有复选框。
使用前面的例子(见清单 15-9 到 15-11),数据集和选项设置是相同的;您唯一需要更改的是在$.jqplot()
函数中传递的数据。在这种情况下,整个数据集将作为参数传递。
var plot1 = $.jqplot ('myChart', [dataSet.data1, dataSet.data2, dataSet.data3, dataSet.data4], options);
让我们删除包含先前控件(单选按钮、滑块)的表格,并用一个包含复选框的新表格来替换它,如清单 15-12 所示(如果你直接从这里开始,你可以复制整个列表而不考虑先前的控件)。此外,除了用于多个系列的四个控件之外,您还可以在末尾添加一个按钮来管理“全部选中/取消选中”功能
清单 15-12。ch15_03.html
<table>
<tr>
<td>
<div>
<ul>
<li><input name="data1" type="checkbox" checked />First series</li>
<li><input name="data2" type="checkbox" checked />Second series</li>
<li><input name="data3" type="checkbox" checked />Third series</li>
<li><input name="data4" type="checkbox" checked />Fourth series</li>
<li><input type="button" name="checkall" value="Uncheck All"></li>
</ul>
</div>
</td>
</tr>
</table>
与单选按钮一样,您必须添加 jQuery 方法来绑定这些控件发生的事件。首先,定义每个复选框的状态。正常情况下都应该检查。然后,定义五个 jQuery 方法,启用或禁用要表示的系列,然后强制 replot。
从代码中,您必须删除所有处理先前控件的行,并在它们的位置上,编写清单 15-13 中的方法。
清单 15-13。ch15_03.html
$("input[type=checkbox][name=data1]").change(function(){
if(this.checked){
plot1.series[0].data = dataSet.data1;
plot1.replot();
} else {
plot1.series[0].data = [];
plot1.replot();
}
});
$("input[type=checkbox][name=data2]").change(function(){
if(this.checked){
plot1.series[1].data = dataSet.data2;
plot1.replot();
} else {
plot1.series[1].data = [];
plot1.replot();
}
});
$("input[type=checkbox][name=data3]").change(function(){
if(this.checked){
plot1.series[2].data = dataSet.data3;
plot1.replot();
} else {
plot1.series[2].data = [];
plot1.replot();
}
});
$("input[type=checkbox][name=data4]").change(function(){
if(this.checked){
plot1.series[3].data = dataSet.data4;
plot1.replot();
} else {
plot1.series[3].data = [];
plot1.replot();
}
});
$("input[type=button][name=checkall]").click(function(){
if(this.value == "Check All"){
plot1.series[0].data = dataSet.data1;
plot1.series[1].data = dataSet.data2;
plot1.series[2].data = dataSet.data3;
plot1.series[3].data = dataSet.data4;
$("input[type=checkbox][name=data1]").prop("checked", true);
$("input[type=checkbox][name=data2]").prop("checked", true);
$("input[type=checkbox][name=data3]").prop("checked", true);
$("input[type=checkbox][name=data4]").prop("checked", true);
this.value = "Uncheck All";
plot1.replot();
} else {
plot1.series[0].data = [];
plot1.series[1].data = [];
plot1.series[2].data = [];
plot1.series[3].data = [];
$("input[type=checkbox][name=data1]").prop("checked", false);
$("input[type=checkbox][name=data2]").prop("checked", false);
$("input[type=checkbox][name=data3]").prop("checked", false);
$("input[type=checkbox][name=data4]").prop("checked", false);
this.value = "Check All";
plot1.replot();
}
});
如图 15-5 所示,用户现在可以选择他或她想要在图表中显示的系列。
图 15-5。
A custom legend with check boxes and a button
如果您单击标记为“全部取消选中”的按钮,所有复选框都将被取消选中,相应的系列将在绘图中隐藏。随后,该按钮将显示标签“全部选中”当这次点击它时,所有的复选框将被选中,相应的系列将显示在图表中。
最后一个例子中涵盖的特性与 EnhancedLegendRenderer 插件提供的图例非常相似(参见第十章中的“处理图例”一节)。在这种情况下,通过单击与系列相对应的彩色方块,您可以决定是否应该在图表中显示该系列。但是,这里您还添加了只需一次点击就可以选中和取消选中所有系列的可能性,而且这个功能目前还没有在插件中实现(尽管有人正在提议)。这是如何通过使用控件来扩展库提供的功能的另一个小例子。
摘要
在本章中,您已经看到了如何使用各种控件(如单选按钮、滑块和复选框)来增加图表的交互性。随着控件的引入,作为程序员,我们不再是唯一能够直接控制图表属性值的人;通过这样的控制,用户也能够做出适当的选择。
此外,您了解了如何将 jQuery UI 小部件与 jqPlot 库集成,并将这些小部件用作控件。在下一章中,您将通过使用 jQuery UI 小部件作为图表的容器来完成这个集成。这种组合极大地扩展了使用 jqPlot 库开发和表示图表的可能性。
十六、在 jQuery 小部件中嵌入 jqPlot 图表
Abstract
在第二章中,您看到了几个用作容器的 jQuery UI 小部件的例子。在本章中,您将利用这种能力来表示这些容器中的图表。这使您能够利用 jQuery UI 小部件的巨大潜力来进一步改进图表的表示方式。
在第二章中,您看到了几个 jQuery UI 小部件被用作容器的例子。在本章中,您将利用这种能力来表示这些容器中的图表。这使您能够利用 jQuery UI 小部件的巨大潜力来进一步改进图表的表示方式。
结合 jQuery UI 和 jqPlot 库的好处是多方面的:可以在网页中显示更多占用相同空间的图表,同时保持页面的上下文整洁。另一个优点是 jQuery UI 小部件可以调整大小,甚至用户可以调整 jqPlot 图表的大小。
在本章中,您将探索三个简单的案例,在这些案例中,刚才提到的好处将变得显而易见。你在使用 jQuery UI 小部件时也会变得更加自信,甚至比你在第二章中做的还要自信。
选项卡上的 jqPlot 图表
你要用作容器的第一个小部件是选项卡(参见第二章的中的“选项卡”一节)。在选项卡内插入图表允许您在同一页面的有限区域内显示不同的图表。在本例中,您将在三个选项卡中放置三个不同的 jqPlot 图表,分别称为Tab 1
、Tab 2
和Tab 3
。在第一个选项卡中,您将放置一个条形图,在第二个选项卡中,您将放置一个多系列折线图,在最后一个选项卡中,您将放置一个饼图。您不会详细分析这些图表,因为它们与前几章中使用的图表完全相同。每种类型的图表都需要特定的插件,清单 16-1 显示了所需插件的列表。
清单 16-1。ch16_01.html
<!--[if lt IE 9]>
<script src="
http://cdn.jsdelivr.net/excanvas/r3/excanvas.js
<![endif]-->
<script src="
http://code.jquery.com/jquery-1.9.1.min.js
<script src="
http://cdn.jsdelivr.net/jqplot/1.0.8/jquery.jqplot.min.js
</script>
<link rel="stylesheet" type="text/css"
href="
http://cdn.jsdelivr.net/jqplot/1.0.8/jquery.jqplot.min.css
<script src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.pieRenderer.min.js
</script>
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins
是
cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.dateAxisRenderer.min.js
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins
是
cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.canvasTextRenderer.min.js
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins
cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.canvasAxisTickRenderer.min.js
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins
cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.categoryAxisRenderer.min.js
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins
cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.barRenderer.min.js
此外,您将使用 jQuery 小部件作为容器,这也需要包含一些文件:
<link rel="stylesheet" href="
http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css
<script src="
http://code.jquery.com/ui/1.10.3/jquery-ui.min.js
Note
对于那些使用本书源代码的工作空间的人来说,可以使用工作空间中已经包含的库。使用以下参考资料:
<script src="../src/js/jquery-1.9.1.js"></script>
<script src="../src/jquery.jqplot.min.js"></script>
<link rel="stylesheet" type="text/css" href="../src/jquery.jqplot.min.css" />
<link rel="stylesheet" href="../src/css/smoothness/jquery-ui-1.10.3.custom.min.css" />
<script src="../src/js/jquery-ui-1.10.3.custom.min.js"></script>
<script src="../src/plugins/jqplot.pieRenderer.min.js"></script>
<script src="../src/plugins/jqplot.dateAxisRenderer.min.js"></script>
<script src="../src/plugins/jqplot.canvasTextRenderer.min.js"></script>
<script src="../src/plugins/jqplot.canvasAxisTickRenderer.min.js"></script>
<script src="../src/plugins/jqplot.categoryAxisRenderer.min.js"></script>
<script src="../src/plugins/jqplot.barRenderer.min.js"></script>
随着网页上引入如此多的图形元素,级联样式表(CSS)样式的使用变得越来越重要。您需要定义一些设置来修改选项卡的外观,使它们符合您的需要。添加清单 16-2 中的样式设置。
清单 16-2。ch16_01.html
<style>
.ui-tabs {
width: 690px;
margin: 2em auto;
}
.ui-tabs-nav {
font-size: 12px;
}
.ui-tabs-panel {
font-size: 14px;
}
.jqplot-target {
font-size: 18px;
}
ol.description {
list-style-position: inside;
font-size: 15px;
margin: 1.5em auto;
padding: 0 15px;
width: 600px;
}
</style>
您将使用三种不同的图表,它们已经在前面的章节中使用过(参见第九章的折线图、第十章的条形图和第十一章的饼图)。因此,这一章不包括它们设置的细节。将它们添加到我们的 web 页面,用chart1
、chart2
和chart3
替换通常的目标名称myChart
。正如你在清单 16-3 中看到的,在这些图表中,你已经直接在三个jqplot()
函数中定义了options
对象。它们的返回值存储在三个不同的变量中:plot1
、plot2
和plot3
。这些变量将用于处理 JavaScript 代码中相应的图表。
清单 16-3。ch16_01.html
var bar1 = [['Germany', 12], ['Italy', 8], ['Spain', 6],
['France', 10], ['UK', 7]];
var data1 = [1, 2, 3, 2, 3, 4];
var data2 = [3, 4, 5, 6, 5, 7];
var data3 = [5, 6, 8, 9, 7, 9];
var data4 = [7, 8, 9, 11, 10, 11];
var pie1 = [
['Dairy', 212], ['Meat', 140], ['Grains', 276],
['Fish', 131], ['Vegetables', 510], ['Fruit', 325]
];
var plot1 = $.jqplot ('chart1', [bar1], {
title: 'Foreigner customers',
series:[{renderer:$.jqplot.BarRenderer}],
axesDefaults: {
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
tickOptions: {
angle: -30,
fontSize: '10pt'
}
},
axes: {
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer
}
}
});
var plot2 = $.jqplot ('chart2', [data1, data2, data3, data4],{});
var plot3 = $.jqplot ('chart3', [pie1], {
seriesDefaults: {
renderer: jQuery.jqplot.PieRenderer,
rendererOptions: {
showDataLabels: true,
dataLabels: 'value',
fill: false,
sliceMargin: 6,
lineWidth: 5
}
}
});
现在是时候在$(document).ready()
函数的末尾添加 jQueryUI tabs()
函数了,如清单 16-4 所示。
清单 16-4。ch16_01.html
$(document).ready(function(){
...
$("#tabs").tabs();
});
这个调用创建了选项卡容器,因此您需要将选项卡绑定到您的绘图上(见清单 16-5)。
清单 16-5。ch16_01.html
$('#tabs').bind('tabsshow', function(event, ui) {
if (ui.index === 0 && plot1._drawCount === 0) {
plot1.replot();
}
else if (ui.index === 1 && plot2._drawCount === 0) {
plot2.replot();
}
else if (ui.index === 2 && plot3._drawCount === 0) {
plot3.replot();
}
});
选择一个选项卡将重新绘制其中的图表内容。现在,在 web 页面的<body>
部分,您需要添加 jQuery UI 库将转换成选项卡的<div>
元素。方法是用tabs
指定一个<div>
元素作为id
。在其中,您定义了一个包含三个项目的列表,每个项目代表一个选项卡。在列表之后,您必须定义另外三个tabs
的子部分:三个额外的<div>
元素,称为tabs-1
、tabs-2
和tabs-3
。你将把这些放到你的图表中:chart1
、chart2
和chart3
(见清单 16-6)。
清单 16-6。ch16_01.html
<div id="tabs">
<ul>
<li><a href="#tabs-1">Tab 1</a></li>
<li><a href="#tabs-2">Tab 2</a></li>
<li><a href="#tabs-3">Tab 3</a></li>
</ul>
<div id="tabs-1">
<p>This is the bar chart</p>
<div id="chart1" style="height:300px; width:650px;"></div>
</div>
<div id="tabs-2">
<p>This is the line chart</p>
<div id="chart2" style="height:300px; width:650px;"></div>
</div>
<div id="tabs-3">
<p>This is the pie chart</p>
<div id="chart3" style="height:300px; width:650px;"></div>
</div>
</div>
图 16-1 显示了最终结果。
图 16-1。
A page with three tabs containing different charts
手风琴上的 jqPlot 图表
另一种常用的 jQuery 容器是 accordion。这一次你将把前三张图表放进手风琴里。web 页面中包含的插件列表与上一个示例中的一样。你需要在 CSS 样式上做一些改变;手风琴有特定的 CSS 类,需要指定它们的属性。它们如清单 16-7 所示。
清单 16-7。ch16_02.html
<style type="text/css">
.ui-accordion {
width: 690px;
margin: 2em auto;
}
.ui-accordion-header {
font-size: 12px;
}
.ui-accordion-content {
font-size: 14px;
}
.jqplot-target {
font-size: 18px;
}
ol.description {
list-style-position: inside;
font-size: 15px;
margin: 1.5em auto;
padding: 0 15px;
width: 600px;
}
.section {
width: 400px;
height: 200px;
margin-top: 20px;
margin-left: 20px;
}
</style>
正如您在前一个示例中所做的那样,您必须创建 jQueryUi 小部件。您可以通过调用accordion()
函数来实现:
$("#accordion").accordion();
您还需要将这个 accordion 绑定到您的图表,如清单 16-8 所示。当您选择一个 accordion 选项卡时,该事件确保在其中重新绘制相应的图表,调用replot()
函数。
清单 16-8。ch16_02.html
$('#accordion').bind('accordionchange', function(event, ui) {
var index = $(this).find("h3").index ( ui.newHeader[0] );
if (index === 0) {
plot1.replot();
}
else if (index === 1) {
plot2.replot();
}
else if (index === 2) {
plot3.replot();
}
});
如您所见,您定义手风琴的方式与您定义选项卡的方式非常相似。以同样的方式,您现在定义了将被转换成 HTML 代码中的折叠标签的<div>
元素(见清单 16-9)。
清单 16-9。ch16_02.html
<div id="accordion" style="margin-top:50px">
<h3><a href="#">Section 1</a></h3>
<div>
<p>This is the bar chart</p>
<div class="section" id="chart1" data-height="200" data-width="400"></div>
</div>
<h3><a href="#">Section 2</a></h3>
<div>
<p>This is the multiseries line chart</p>
<div class="section" id="chart2" data-height="200" data-width="400"></div>
</div>
<h3><a href="#">Section 3</a></h3>
<div>
<p>This is the pie chart</p>
<div class="section" id="chart3" data-height="200" data-width="400"></div> </div>
</div>
在图 16-2 中可以看到,结果与上一个类似,但这次不同的图表是通过垂直滑动 accordion 选项卡来替换的。
图 16-2。
An accordion widget containing three charts
可调整大小和可拖动的图表
您可以在图表中广泛利用的另外两个功能允许用户调整和拖动容器区域。网页中可调整大小的框架允许您任意更改其大小及其包含的对象的大小。这个特性可以与在页面中拖动元素的能力相结合,这将使它们能够相对于原始元素占据不同的位置。
除了赋予页面布局流动性之外,当您希望用户交互地管理页面上不同框架所占据的空间时,该功能有时会很有用(参见图 16-3 )。
图 16-3。
Enclosing the charts in jQueryUI containers enables you to resize and move them around the page
在本节中,您将看到两个示例。在第一个示例中,您将关注应用于折线图的大小调整。您将看到调整包含在容器中的图表的大小是多么容易。在第二个示例中,您将通过添加两个折线图来进一步开发该示例。一旦所有三个图表都启用了 draggable 属性,您将看到如何根据自己的喜好更改它们的位置,甚至交换它们。
可调整大小的折线图
在本例中,您将使用一个简单的折线图。因此,除了 jQuery 容器和基本 jqPlots 库所需的插件之外,您不再需要包含所有的 jqPlot 插件:
<!--[if lt IE 9]>
<script type="text/javascript" src="../src/excanvas.js"></script>
<![endif]-->
<script type="text/javascript" src="../src/jquery.min.js"></script>
<script type="text/javascript" src="../src/jquery.jqplot.min.js"></script>
<link rel="stylesheet" type="text/css" href="../src/jquery.jqplot.min.css" />
<link rel="stylesheet" href="../src/css/smoothness/jquery-ui-1.10.3.custom.min.css" />
<script src="../src/js/jquery-ui-1.10.3.custom.min.js"></script>
或者,如果您喜欢使用内容交付网络(CDN)服务:
<!--[if lt IE 9]><script type="text/javascript" src="
http://cdn.jsdelivr.net/excanvas/r3/excanvas.js
<script src="
http://code.jquery.com/jquery-1.9.1.min.js
<script type="text/javascript" src="
http://cdn.jsdelivr.net/jqplot/1.0.8/jquery.jqplot.min.js
<link rel="stylesheet" type="text/css" href="
http://cdn.jsdelivr.net/jqplot/1.0.8/jquery.jqplot.min.css
<link rel="stylesheet" href="
http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css
<script src="
http://code.jquery.com/ui/1.10.3/jquery-ui.min.js
即使在这里也有必要指定一些 CSS 样式,如清单 16-10 所示。
清单 16-10。ch16_03.html
<style type="text/css">
.chart-container {
border: 1px solid darkblue;
padding: 30px 0px 30px 30px;
width: 900px;
height: 400px;
}
#chart1 {
width: 96%;
height: 96%;
}
</style>
在网页的<body>
部分,你现在添加了<div>
元素,它将是包含名为chart1
的折线图的容器(见清单 16-11)。
清单 16-11。ch16_03.html
<div class="chart-container">
<div id="chart1"></div>
</div>
现在,在将chart-container
定义为容器之后,可以用两个 jQuery 方法来处理它——resizable()
函数添加了可调整大小的功能,而bind()
函数将调整大小的事件绑定到图表的重新绘制上(参见清单 16-12)。
清单 16-12。ch16_03.html
$(document).ready(function(){
var plot1 = $.jqplot ('chart1', [[100, 110, 140, 130, 80, 75, 120, 130, 100]]);
$('div.chart-container').resizable({delay: 20});
$('div.chart-container').bind('resize', function(event, ui) {
plot1.replot();
});
});
结果是一个可调整大小的图表,如图 16-4 所示,在右下角有一个灰色的小三角形。通过点击它,用户可以调整容器的大小,从而调整 jqPlot 图表的大小。
图 16-4。
A resizable line chart
三个可拖动的折线图
从上一个示例开始,您将通过将折线图放在两个独立的容器中来添加两个折线图。这里的目标——除了使所有三个容器都可以调整大小之外——是使容器可以拖动。最终结果是一个包含三个折线图的网页,可以通过拖动它们来改变它们的位置,甚至交换它们的位置。
首先对前面的例子做一些小的补充。在清单 16-13 中,您添加了另外两个容器(chart-container2
和chart-container3
),里面有新的折线图,分别命名为chart2
和chart3
。
清单 16-13。ch16_03b.html
<BODY>
<div class="chart-container">
<div id="chart1"></div>
</div>
<div class="chart-container2">
<div id="chart2"></div>
</div>
<div class="chart-container3">
<div id="chart3"></div>
</div>
</BODY>
现在你已经为新的折线图创建了容器,有必要通过三个不同的$.jqplot()
函数来定义它们(见清单 16-14)。这三个函数返回的值将被传递给三个变量:plot1
、plot2
和plot3
。这是因为每当容器发生变化时,您需要通过对这三个变量使用replot()
函数来重新绘制这三个图表。
清单 16-14。ch16_03b.html
$(document).ready(function(){
var plot1 = $.jqplot ('chart1', [[100, 110, 140, 130, 80, 75, 120, 130, 100]],
{seriesColors: [ "#bb0000" ]});
var plot2 = $.jqplot ('chart2', [[120, 90, 150, 120, 110, 75, 90, 120, 110]],
{seriesColors: [ "#00bb00" ]});
var plot3 = $.jqplot ('chart3', [[ 130, 110, 140, 100, 80, 135, 120, 90, 110]],
{seriesColors: [ "#0000bb" ]});
$('div.chart-container').resizable({delay:20});
...
});
现在,您将为这三个容器激活可拖动特性。做到这一点真的很简单;您需要将该函数添加到应用于每个容器的三个 jQuery 选项中,如清单 16-15 所示。此外,您将为两个新容器添加调整大小功能,其方式与第一个容器相同。
清单 16-15。ch16_03b.html
$(document).ready(function(){
...
var plot3 = $.jqplot ('chart3', [[130, 110, 140, 100, 80, 135, 120, 90, 110]],
{seriesColors: ["#0000bb"]});
$('div.chart-container').draggable({cursor: 'move'});
$('div.chart-container2').draggable({cursor: 'move'});
$('div.chart-container3').draggable({cursor: 'move'});
$('div.chart-container').resizable({delay: 20});
$('div.chart-container').bind('resize', function(event, ui) {
plot1.replot();
});
$('div.chart-container2').resizable({delay: 20});
$('div.chart-container2').bind('resize', function(event, ui) {
plot2.replot();
});
$('div.chart-container3').resizable({delay: 20});
$('div.chart-container3').bind('resize', function(event, ui) {
plot3.replot();
});
});
剩下的就是添加 CSS 样式,从而定义每个容器的初始位置和大小,如清单 16-16 所示。
清单 16-16。ch16_03b.html
<style type="text/css">
.chart-container {
border: 1px solid darkblue;
padding: 30px 0px 30px 30px;
width: 300px;
height: 200px;
position: relative;
float: left;
}
.chart-container2 {
border: 1px solid darkblue;
padding: 30px 0px 30px 30px;
width: 200px;
height: 200px;
position: relative;
float: left;
margin-left: 20px;
}
.chart-container3 {
border: 1px solid darkblue;
padding: 30px 0px 30px 30px;
width: 500px;
height: 200px;
position: relative;
float: left;
margin-left: 20px;
}
#chart1 {
width: 96%;
height: 96%;
}
#chart2 {
width: 96%;
height: 96%;
}
#chart3 {
width: 96%;
height: 96%;
}
</style>
在图 16-5 中,你可以看到页面初始加载时的页面布局。图 16-6 显示了用户改变了第三个图表的位置和大小,使其位于其他两个图表的下方。
图 16-6。
By dragging and resizing the containers, the original layout can be changed
图 16-5。
The web page shows the three line charts enclosed in three different containers
摘要
在本章中,您已经看到了如何利用 jQuery UI 库提供给您的小部件的潜力,这些小部件可以帮助您改进图表的表示方式。您已经看到了如何在容器中包含更多的图表,例如折叠和标签,以便您可以逐个查看它们,即使它们占据相同的区域。您还看到了如何调整这些容器的大小,将这种功能扩展到用 jqPlot 库开发的图表。
到目前为止,你已经加深了你的图表的图形和代表性方面。在下一章,你将学习图表的核心:数据管理。到目前为止,为了使示例更容易理解,页面中定义的数据种类是有限的。实际上,在包含代码管理图表的同一个网页上定义数据是不太可能的。更有可能的是,数据是由外部文件或数据库通过 SQL 查询提供的。
十七、处理输入数据
Abstract
一旦您处理了图表的所有图形方面,就该更详细地分析输入数据了。在前面的章节中,您将输入数据的值赋给了数组。这些数组是在 jqPlot 代码所在的同一个 HTML 页面中定义的。你经常使用这两种方式:
一旦您处理了图表的所有图形方面,就该更详细地分析输入数据了。在前面的章节中,您将输入数据的值赋给了数组。这些数组是在 jqPlot 代码所在的同一个 HTML 页面中定义的。你经常使用这两种方式:
var plot1 = $.jqplot ('chart1', [[100, 110, 140, 130, 80, 75, 120, 130, 100]]);
和
var data = [[100, 110, 140, 130, 80, 75, 120, 130, 100]];
实际上,为了获得这样的数据,经常需要与其他技术进行交互,为此,您需要找到一种非常适合任何数据源的方法。由于需要使用一种通用的文本格式,这种格式可以被不同的脚本语言(尤其是 JavaScript)轻松处理,并且仍然能够被人类理解,因此人们开始使用 JavaScript 对象表示法(JSON)格式。您已经在第一章中简要地了解了这种格式,但是现在您将看到如何具体地使用它来处理来自外部来源的输入数据。
本章详细研究了 JSON 格式,首先说明了这种格式的数据结构,然后向您展示了如何在 jqPlot 库中使用它们。为此,您将看到两种不同的处理 JSON 数据的方法——第一种使用 jqPlot 插件,第二种使用专门解析 JSON 数据的 jQuery 函数。
无论来自外部源的数据是如何构建的,如果您想要对真实数据的管理和处理有一个完整的概述,您还需要考虑这些数据是如何生成的以及随之而来的采集模式。因此,在本章的最后一部分,您将专门使用 jqPlot 库开发一个实时图表。
事实上,不管输入数据的格式如何,很多时候数据源不仅是外部的,而且是连续的—输入数据由一系列数据组成,这些数据中的值一次产生一个,连续不断。因此,显示这种类型数据的图表不仅必须管理来自外部源的数据格式,还需要能够不断更新自身,从而确保数据表示(在这种情况下,是实时图表)总是更新的。
使用 JSON 格式
本节介绍 JSON 格式,包括用于库 jqPlot 的各种选项。首先,通过分析一些语法图,您将了解 JSON 格式的结构化数据是如何形成的。然后你将继续学习实际的例子。
JSON 格式
JSON 是一种数据交换格式。由于它的树型结构,其中每个元素都被称为一个名称-值对,所以人类很容易读写它,机器也很容易解析和生成它。这是它越来越普遍使用的主要原因。
JSON 结构建立在两种不同结构的组合之上:数组和对象(见图 17-1 )。在其中,您可以定义所有常用的经典原始值,甚至在其他语言中也是如此:数字、布尔值、字符串和空值。这允许包含在其中的值在各种编程语言之间交换。(在 www.json.org
,您可以找到处理 JSON 格式的所有语言的列表,以及所有相关技术的列表,比如库、模块、插件等等。)
图 17-1。
Syntax diagrams for JSON
为了更好地理解图 17-1 中的语法图,你可以分析 JSON 格式是如何构造的。你必须考虑两件事。首先,对象和数组都包含一系列由图中的value
标签标识的值。value
指任意类型的值,比如字符串、数字、布尔值,甚至可以是对象、数组。
除此之外,你很容易猜到 JSON 结构是一个不同层次的树形结构。该树将数组或对象作为节点;树叶是其中包含的值。
考虑一些例子。如果您有一个只有一层的 JSON 结构,您将只有两种可能:
- 一组值
- 有值的对象
如果将该结构扩展到两个级别,则有四种可能性(为简单起见,假设该树是对称的):
- 数组的数组
- 一组对象
- 带有数组的对象
- 有对象的对象
诸如此类;案件逐渐变得更加复杂。
经典的 JSON 结构恰恰是你在本书中已经经常用到的 jqPlot 库options
对象的结构。事实上,您已经看到,由于对象有一个与每个值相关联的字符串,这些树结构可以描述任何类型的元素。即使是非常复杂的元素,如图表,也可以很容易理解和操作。
一个实际案例:jqPlot 数据渲染器
将 JSON 作为一种交换格式,本节考虑外部数据源是文本文件的简单情况。在本例中,您将使用由 jqPlot 库:json2
插件直接呈现的数据。这个插件允许您读取文件中包含的 JSON 格式的数据,以便将它们用作输入数据。对于您来说,唯一需要做的事情就是将外部源分配给dataRenderer
属性。
首先实现一个空白的 HTML 页面,如清单 17-1 所示。
清单 17-1。ch17_01a.html
<HTML>
<HEAD>
<TITLE>Chapter 17</TITLE>
<!--[if lt IE 9]>
<script type="text/javascript" src="../src/excanvas.js"></script>
<![endif]-->
<script type="text/javascript" src="../src/jquery.min.js"></script>
<script type="text/javascript" src="../src/jquery.jqplot.min.js"></script>
<link rel="stylesheet" type="text/css" href="../src/jquery.jqplot.min.css" />
<script>
$(document).ready(function(){
//Add the JavaScript code here
});
</script>
</HEAD>
<BODY>
<div id="myChart" style="height:300px; width:500px;"></div>
</BODY>
</HTML>
为了让数据呈现器解释数据,外部源必须返回有效的 jqPlot 数据数组。要用这个功能扩展图表,您需要包含jqplot.json2
插件:
<script type="text/javascript" src="../src/plugins/jqplot.json2.min.js"></script>
或者使用内容交付网络(CDN)服务:
<script type="text/javascript"
src="
http://cdn.jsdelivr.net/jqplot/1.0.8/plugins/jqplot.json2.min.js
作为外部源,您可以选择包含数据的 TXT 文件。对于本例,您将使用记事本或任何其他文本编辑器创建一个新的 TXT 文件。编辑完清单 17-2 中报告的数据后,将文件另存为jsondata.txt
。
清单 17-2。jsondata.txt
[[30, 12, 24, 54, 22, 11, 64, 33, 22]]
在您想要管理外部文件中的数据的网页上,您需要添加清单 17-3 中的代码。
清单 17-3。ch17_01a.html
$(document).ready(function(){
var ajaxDataRenderer = function(url, plot, options) {
var ret = null;
$.ajax({
async: false,
url: url,
dataType:"json",
success: function(data) {
ret = data;
}
});
return ret;
};
var jsonurl = "./jsondata.txt";
var options = {
title: "AJAX JSON Data Renderer",
dataRenderer: ajaxDataRenderer,
dataRendererOptions: {
unusedOptionalUrl: jsonurl
}
};
$.jqplot('myChart', jsonurl,options);
});
您将得到如图 17-2 所示的图表,该图表直接从 TXT 文件中获取数据。
图 17-2。
A line chart representing data from the jsondata.txt
file
如果你想插入一个以上的序列,TXT 文件中的数据格式与输入数据数组的格式保持一致(见清单 17-4)。用编辑器将这些数据复制到一个文件中后,将该文件另存为jsondata2.txt
。
清单 17-4。jsondata2.txt
[[1,3,2,4,3,4,1,2],
[6,7,9,6,8,9,10,9],
[15,12,11,9,11,12,13,14]]
图 17-3 显示了从 TXT 文件中读取的三个系列的图表。
图 17-3。
A multiseries line chart representing data from a TXT file
JSON 和$。getJSON()
还有另一种方法可以在 jqPlot 图表中使用外部 JSON 数据。jQuery 没有使用json2 jqPlot
插件作为数据呈现器,而是提供了一个执行相同功能的方法;叫做$.getJSON()
。
这个方法读取一个 JSON 文件并解析它。它还可以通过发出 HTTP GET
请求直接从服务器加载 JSON 编码的数据。它广泛应用于 Web 上的许多应用,而不仅仅是 jqPlot。该方法有三个参数:
$.getJSON(url, data, success(data, textStatus, jqXHR));
只有url
是强制的;另外两个参数是可选的。url
是一个字符串,包含 JSON 文件的 URL 或请求的服务器的 URL。data
是一个随请求发送到服务器的字符串,success()
是一个回调函数,如果请求成功,将执行该函数。
文件中包含的数据必须遵循 JSON 编码规则。因为您使用它们是为了通过 jqPlot 进行编码,所以它们应该具有以下格式:
{ "series_name1": [ value1, value2, value3, ...],
"series_name2": [ value1, value2, value3, ...], ... }
创建一个新的 TXT 文件并另存为jsondata3.txt
。这个文件包含四个不同系列的数据,如清单 17-5 所示。
清单 17-5。jsondata3.txt
{"data1": [1,2,3,2,3,4],
"data2": [3,4,5,6,5,7],
"data3": [5,6,8,9,7,9],
"data4": [7,8,9,11,10,11]}
下一步是调用getJSON()
方法:
$.getJSON('./jsondata3.txt', '', myPlot);
你一定要注意写对网址。在这个例子中,TXT 文件和 HTML 文件在同一个目录下,所以需要在文件名前加上./
作为前缀。第二个参数是一个空字符串,因为您不需要向 URL 发送任何数据(它是一个文件,而不是服务器应用)。myPlot
是函数的返回值,检查$.jqplot()
的加载是否良好。正如你在清单 17-6 中看到的,你只需要添加你自己的函数,其中你已经定义了$.jqplot()
函数。在这种情况下,要传递的数据(如data1
、data2
等)属于数据对象,因此必须作为data.data1
、data.data2
等进行传递。
清单 17-6。ch17_02.html
$(document).ready(function(){
var myPlot = function (data, textStatus, jqXHR) {
$.jqplot ('myChart',[data.data1, data.data2, data.data3, data.data4]);
};
$.getJSON('./jsondata3.txt', '', myPlot);
});
这样你就获得了一个多系列折线图(见图 17-4 ),就好像你直接在网页上写数据一样,但是使用服务器和其他应用的机会极大地扩展了你的能力。
图 17-4。
A multiseries line chart representing data on a TXT file
实时图表
实时图表会自动更新自身,从而使您能够表示来自持续产生数据的来源的数据流。该来源可以是服务器、应用、连接到 PC 的设备等等。在这种情况下,图表就扮演了真正的指示器的角色,也就是说,它是一种提供某种特性如何随时间变化的直观指示的工具。
您现在将只使用 jqPlot 库开发一个简单的实时折线图。例如,假设您想要实现一个幅度从 0 到 100%不等的指示器。例如,这个量可以是资源(如 CPU)的消耗量,但也可以应用于许多其他方面,如温度、参与者或连接的数量等等。在这种情况下,您将从 50%的值开始,并实时生成随机变化,这只是为了模拟数据源。只要用一个从外部获取数据的函数替换 random 函数,就可以将这个例子应用到任何其他情况。
您将实现一个网页,其中显示了一个折线图,该图表中只有一个设置为 50%的小拉伸值(参见图 17-5 )。这将是流数据值的起点。
图 17-5。
The real-time chart before acquiring data shows only a small stretch as its starting point
清单 17-7 显示了获得图 17-5 所示图表所需的全部内容。在options
中,您定义轴范围的限制,以便在图表更新期间没有变化。为了给动画一个更流畅的效果,您删除了线上的标记并启用了smooth
模式。在图表下,您将插入一个按钮来开始实时更新。
清单 17-7。ch17_03.html
<HTML>
<HEAD>
<TITLE>Real-time chart</TITLE>
<!--[if lt IE 9]>
<script type="text/javascript" src="../src/excanvas.js"></script>
<![endif]-->
<script type="text/javascript" src="../src/jquery.min.js"></script>
<script type="text/javascript" src="../src/jquery.jqplot.min.js"></script>
<link rel="stylesheet" type="text/css" href="../src/jquery.jqplot.min.css" />
<script class="code" type="text/javascript">
$(document).ready(function(){
data = [50, 50];
var options = {
axes: {
xaxis: {min: 1, max: 21, numberTicks: 5},
yaxis: {min: 0, max: 100,numberTicks: 6,
tickOptions:{formatString: '%d%'}
}
},
seriesDefaults: {
showMarker: false,
rendererOptions: {smooth: true}
}
};
var plot1 = $.jqplot ('myChart', [data], options);
});
</script>
</HEAD>
<BODY>
<div id="myChart" style="height: 300px; width: 500px;"></div>
<button>Start Updates</button>
</BODY>
</HTML>
插入清单 17-8,您现在捕获按钮的click
事件,并将其链接到doUpdate()
函数的执行。一旦按钮被按下,你就可以把它从网页上删除。
清单 17-8。ch17_03.html
$(document).ready(function(){
...
var plot1 = $.jqplot ('myChart', [data],options);
$('button').click( function(){
doUpdate();
$(this).hide();
});
});
因此,在清单 17-9 中,您实现了生成随机变量的函数。
清单 17-9。ch17_03.html
$(document).ready(function(){
...
$('button').click( function(){
doUpdate();
$(this).hide();
});
function getRandomInt (min, max){
return Math.floor(Math.random() * (max - min + 1)) + min;
}
});
此函数生成最小值和最大值之间的整数值(可以是负值)。这些值作为参数传递给函数。您在-3 和 3 之间设置了一个可能的变量,该变量将应用于最后获取的值。实时值存储在一个名为data
的数组中,该数组作为一种缓冲区运行。该数组仅包含 20 个值,因此第一个(最大的)值将被删除,新获取的值将被插入数组的最后一个位置。正如你在图 17-4 中看到的,开始时你会看到一条振荡线,它延伸了图表的长度。然后线的右端将移动,跟随观察到的震级的趋势。
要获得动画,您需要刷新图表,因此每次更新您都需要销毁当前图表(plot1
),用新的数据数组替换数据数组,然后重新绘制整个plot1
图表。最后,您需要调用setTimeout()
函数,该函数将再次调用doUpdate()
函数。如此,循环无休止地重复。您可以每秒(1,000 毫秒)更新图表,但在其他情况下,这些值将根据源数据进行选择。
继续将清单 17-10 添加到您的代码中。
清单 17-10。ch17_03.html
$(document).ready(function(){
...
function getRandomInt (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function doUpdate() {
var last = data[data.length-1];
if(data.length > 19){
data.shift();
}
var newlast = last + getRandomInt(-3, 3);
if(newlast < 0)
newlast = 0;
data.push(newlast);
if (plot1) {
plot1.destroy();
}
plot1.series[0].data = data;
plot1.replot( {resetAxes: true} );
plot1 = $.jqplot ('myChart', [data], options);
setTimeout(doUpdate, 1000)
}
});
图 17-6 展示了实时图表如何显示围绕 50%值变化的数据流。
图 17-6。
A real-time line chart representing values moment by moment
摘要
在这一章中,你已经看到了输入数据通常来自外部资源,以及如何处理它们。通过一种特殊的方式,您已经看到了如何将 JSON 格式的外部数据用作用 jqPlot 库开发的图表的输入数据数组。关于实时生成的数据的管理,您已经看到了一个简单而有效的示例,其中您实现了一个实时折线图,该折线图随着所获取的数据的变化而更新。
在下一章,你将认识另一个图书馆。在许多方面,它与 jqPlot 非常相似,它被称为 Highcharts。通过许多例子,您将看到这个商业库如何保持 jqPlot 的许多基本特性,而且还极大地扩展了您的可能性并为您的图表添加了更多特性。您很快就会看到,Highcharts 库提供了比您目前所看到的更多的功能。
十八、从 jqPlot 到 Highcharts
Abstract
跟随 jqPlot 框架的脚步,一个新的 JavaScript 库正在迎头赶上,它叫做 Highcharts。这是一个商业产品,由挪威公司 Highsoft Solutions AS 于 2009 年底完成。在撰写本书时,这个新库的版本是 3.0.1,并且越来越多地作为图表专业表示的新解决方案在市场上提供。
跟随 jqPlot 框架的脚步,一个新的 JavaScript 库正在迎头赶上,它叫做 Highcharts。这是一个商业产品,由挪威公司 Highsoft Solutions AS 于 2009 年底完成。在撰写本书时,这个新库的版本是 3.0.1,并且越来越多地作为图表专业表示的新解决方案在市场上提供。
因为我们已经在很多方面提到了 jqPlot 的语法和结构,所以在这本书的第一部分末尾添加 Highcharts 库,专门用一章来介绍它,似乎是合适的。这个框架继承了 jqPlot 的所有优点:只需添加几行代码就可以在网页上实现一个完整的图表。Highcharts 还能够与许多其他 JavaScript 框架接口,包括 jQuery。就像 jqPlot 一样,它的语法简单、重要且直观,但不排除进一步扩展和定制的可能性。这还不是全部。
Highcharts 是一个专业产品,所以它远远超出了 jqPlot 库所能提供的范围。在这一章中,您将看到如何从您已经在 jqPlot 中看到的案例开始,向您的图表添加更多的特性。事实上,在第一个示例中,您将看到如何扩展已经很好的图形功能,这要归功于 jqPlot 库。您将通过进一步增强图表元素来做到这一点,比如工具提示、标记点和网格。此外,您将发现这个库如何包含许多主题,并了解如何将它们应用到您的图表中,以使它看起来总是与众不同。另一个例子说明了 Highcharts 库如何直接从文件中读取数据。最后,在本章的最后一部分,您将进一步创建三种非常特殊的图表,如下所示:
- 主从图表——当您需要可视化大量数据,但希望一次只分析一个细节时,这是一个非常有用的解决方案。
- 甘特图——一种特别适合于安排流程和项目的图表。
- 组合图表—一个非常丰富的可视化工具,用于组合不同类型的图表。
多亏了 Highcharts 库,这三种图表类型才成为可能。因此,通过放置和逐步插入这些新特性,准备好在您离开 jqPlot 的地方熟悉这个漂亮的库。
高图表分布
关于 Highcharts 的使用,本产品有商业和免费的非商业许可。显然,Highcharts 库的免费使用仅限于个人和非营利目的。
在下载了构成发行版的文件包之后,您可以看到它包含了一大组文件。(更多信息见附录 A。)。)
主文件是highcharts.js
,它是这个框架的真正核心,如果你想用 Highcharts 来绘制你的图表,在你的网页中包含这个文件是必不可少的。包含它的方式总是相同的,您可以通过两种方式完成:通过本地方法或内容交付网络(CDN)服务。
对于本地方法,请使用以下内容:
<script type="text/javascript" src="../src/js/highcharts.js"></script>
你有两个 CDN 服务选项。如果您想要最新的稳定版本,请写:
<script src="
http://code.highcharts.com/highcharts.js
否则,如果您想要包含特定版本,请使用以下命令:
<script src="
http://code.highcharts.com/3.0.5/highcharts.js
此外,除了highcharts.js
之外,您还需要包含 jQuery,因为与 jqPlot 一样,它是基于那个库构建的,因此需要您访问它。即使在这种情况下,您也可以选择本地解决方案:
<script type="text/javascript" src="../src/js/jquery-1.9.1.js"></script>
或者 CDN 解决方案:
<script type="text/javascript" src="
http://code.jquery.com/jquery-1.9.1.min.js
</script>
然而,通过使用发行版中包含的一些适配器文件,也可以使用其他库作为基础而不是 jQuery。因此,您可以用 MooTools 或 Prototype 等库来替换 jQuery。
当您深入研究发行版时,您会看到另一个名为highcharts-more.js
的文件。该文件包含与库的某些扩展和特殊情况相关的所有功能,例如,开发仪表、范围和极坐标图所需的功能。此外,有一组文件扮演着与 jqPlots 中的插件类似的角色,因为这些文件中的每一个都为您的图表添加了特定的功能。你可以在目录/js/modules
中找到它们。表 18-1 描述了模块文件。
表 18-1。
The modules in the Highcharts distribution (v.3.0.5)
| 组件 | 描述 | | --- | --- | | `annotations.js` | 向图表元素添加注释的实用程序。 | | `canvas-tools.js` | 不支持 SVG 的 Android 2.x 设备的附加文件。它包含了用于 SVG 的 JavaScript 解析器`canvg`。 | | `data.js` | 一个实用程序,用于将输入源(如 CSV、HTML 表或网格视图)解析为基本配置选项,以便在 Highcharts 构造函数中直接使用。 | | `exporting.js` | 它允许用户下载 PDF、PNG、JPEG 或 SVG 矢量图像格式的图表。 | | `funnel.js` | 绘制漏斗图时需要。 | | `Heatmap.js` | 绘制热图时需要。 | | `map.js` | 绘制地理地图所必需的。 |最后,你可以找到一个名为/js/themes
的包含主题的目录,在表 18-2 中有描述。稍后将在单独的一节中对它们进行更详细的介绍。
表 18-2。
The themes in the Highcharts distribution (v.3.0.5)
| 主题 | 描述 | | --- | --- | | `dark-blue.js` | 它为图表添加了深蓝色背景。该系列的色彩鲜艳。 | | `dark-green.js` | 它为图表添加了深绿色背景。该系列的色彩鲜艳。 | | `gray.js` | 它为图表添加了黑色背景。该系列的色彩鲜艳。 | | `grid.js` | 它强化了图表下方的网格。该系列的色彩鲜艳。 | | `skies.js` | 它添加了一个天空和一些云作为背景。这个系列的颜色都是深色的。 |异同
本节探讨了 jqPlot 库(您现在已经很熟悉了)和新的 Highcharts 库之间的异同。您会发现许多共同点,既包括您定义期权结构的方式,也包括许多其他方面,如数据处理。
但是,为了更好地理解这两个库之间的相似之处和不同之处,您将研究一个多系列折线图,并且使用相同的数据集,您将生成两个表示(每个库一个)。因此,分析这两种情况,您将比较代码的结构和表示本身。
从你之前在 jqPlot 中看到的基本多系列折线图的例子开始(见清单 18-1)。
清单 18-1。ch18_01x.html
<script type="text/javascript" src="../src/jquery.min.js"></script>
<script type="text/javascript" src="../src/jquery.jqplot.min.js"></script>
<link rel="stylesheet" type="text/css" href="../src/jquery.jqplot.min.css" />
<script type="text/javascript">
$(document).ready(function(){
var series1 = [1, 2, 3, 2, 3, 4];
var series2 = [3, 4, 5, 6, 5, 7];
var series3 = [5, 6, 8, 9, 7, 9];
var series4 = [7, 8, 9, 11, 10, 11];
$.jqplot ('myChart', [series1, series2, series3, series4]);
});
</script>
您可以使用 Highcharts 库来构建它的实际等价物,如清单 18-2 所示。
清单 18-2。ch18_01a.html
<script type="text/javascript" src="../src/js/jquery-1.9.1.js"></script>
<script type="text/javascript" src="../src/js/highcharts.js"></script>
<script>
var series1 = [1, 2, 3, 2, 3, 4];
var series2 = [3, 4, 5, 6, 5, 7];
var series3 = [5, 6, 8, 9, 7, 9];
var series4 = [7, 8, 9, 11, 10, 11];
$(function () {
$('#myChart').highcharts({
chart: {
type: 'line'
},
series: [{ data: series1 },
{ data: series2 },
{ data: series3 },
{ data: series4 }]
});
});
</script>
语法上的相似之处非常明显。作为参数传递给highcharts()
函数的结构与您在 jqPlot 中用来定义为options
的结构非常相似。在 Highcharts 中,这个结构被定义为一个配置对象。没有什么能阻止你通过一个options
变量在外部定义它,就像你用 jqPlot 做的那样,如清单 18-3 所示。
清单 18-3。ch18_01a.html
var options = {
chart: {
type: 'line'
},
series: [{ data: series1 },
{ data: series2 },
{ data: series3 },
{ data: series4 }]
}
$(function () {
$('#myChart').highcharts(options);
});
加载相同的数据,但是使用两个不同的库,你得到如图 18-1 所示的图表。
图 18-1。
Comparison between the two line charts generated with the jqPlot (left) and Highcharts (right) libraries
从图 18-1 可以看出两个图表的布局差别很大。此外,在其基本配置中,Highcharts 库提供了一个标题、一个图例和轴标签,尽管没有明确指定这些内容。此外,甚至工具提示也是活动的,事实上,如果您将鼠标移动到线的点上,工具提示会立即出现,报告与该点相关的信息。
图 18-2。
By default, a tooltip reports the x and y values of the point and the name of the series to which it belongs
此外,当页面加载图表时,它是用动画从左到右逐渐绘制的。默认情况下,还会添加一个图例,对应 jqPlot 的enhancedLegendRenderer
插件生成的图例(参见第十章中的“处理图例”部分);事实上,图例中的项目是活动的,如果您单击它们,相应的系列将从图表中删除。此外,一个非常重要的细节,轴和网格的比例会自动更新。它们适应剩余器械包覆盖的新范围,以优化显示效果。这是一种渐进效果,默认情况下包含在内。
所以很明显,高图表的起点几乎是 jqPlot 的顶点。事实上,正如您将在后面看到的,这个库将进一步扩展您的能力,而不需要接触代码,或者编写额外的函数或 JavaScript 库。
从基本代码来看,和 jqPlot 中的options
一样,Highcharts 中的配置对象是由很多组件组成的。在这些组件中,有一整套预先填充了默认值的属性。如果没有明确指定这些属性,则图表将使用默认值。该行为几乎与 jqPlot 的options
相同。这为使用这些库的开发人员提供了相当大的优势。尽管这些库没有指定所有的属性,但是它们代表了最常见的图表类型。这极大地方便了开发人员的工作,尤其是在缺乏经验的时候。
对象配置中最常用的组件对象有:
chart
—其中属性定义为布局、事件、动画、相对边距和绘图区域大小series
—指定与数据系列及其属性相对应的数组- x 轴和 y 轴-在这里定义轴和轴标签的属性
- 标题和副标题—在此插入图表的标题和最终副标题
legend
—指定图例的所有内容tooltip
—指定工具提示的所有内容
如您所见,所有这些组件都已经很熟悉了。因此,在本章中,您不会对它们进行更深入的研究。那些希望深入研究配置对象组件的人可以参考 Highcharts 库官方网站上的完整文档( http://api.highcharts.com/highcharts
)。
带高点图的折线图
您首先考虑一个简单的折线图,以理解 jqPlot 和 Highcharts 库之间的相似之处和不同之处。正如对 jqPlot 库所做的那样,本节通过更详细地处理折线图的实现来介绍 Highcharts 库。
首先,您将通过添加更多元素来完成简单的折线图。稍后,您将看到定义输入数据的不同方式,特别是通过管理它们的类别和范围。因此,您将看到更多的图形方面,比如网格、工具提示和图例,以及如何定制这些元素。最后,您将了解这个库提供的主题,包括它们是什么以及如何使用它们。
完成折线图
您刚刚使用 Highcharts 创建的折线图是在没有在配置对象中定义任何属性的情况下生成的,它只是突出显示了默认情况下来自 Highcharts 库的所有内容。首先,您将定义一组完成图表的属性,然后您将添加更多属性,用一些 jqPlot 库并不总是提供的新特性来丰富图表。因此,作为第一步,将清单 18-4 中用粗体突出显示的属性添加到您的基本代码中。
清单 18-4。ch18_01b.html
<script type="text/javascript" src="../src/js/jquery-1.9.1.js"></script>
<script type="text/javascript" src="../src/js/highcharts.js"></script>
<script>
var series1 = [1, 2, 3, 2, 3, 4];
var series2 = [3, 4, 5, 6, 5, 7];
var series3 = [5, 6, 8, 9, 7, 9];
var series4 = [7, 8, 9, 11, 10, 11];
$(function () {
$('#myChart').highcharts({
chart: {
type: 'line',
marginTop: 60,
marginLeft: 60
},
title: {
text: 'Central Art Museum',
x: 10
},
subtitle: {
text: '27th April,2013',
x: 10
},
xAxis: {
title: {
text: 'Time',
x: 100
},
categories:
['9:00', '11:00', '13:00', '15:00', '17:00', '19:00'],
tickmarkPlacement: 'on'
},
yAxis: {
title: {
text: 'Visitors'
}
},
series: [{
data: series1,
name: 'Australia'
}, {
data: series2,
name: 'Belgium'
}, {
data: series3,
name: 'Canada'
}, {
data: series4,
name: 'Danmark'
}],
legend: {
layout: 'vertical',
align: 'right',
verticalAlign: 'top',
y: 60
}
});
});
</script>
<div id="myChart" style="width: 600px; height: 400px;"></div>
你会得到图 18-3 中的折线图。
图 18-3。
A complete line chart with the Highcharts library
处理输入数据的不同方式
通常,具有两个坐标(x,y)的输入数据可以用两种不同的方式处理。如果 x 的值的序列对于所有的序列都是相同的,如正在讨论的例子,最好是分别处理坐标。您为每个序列定义一个 y 值数组,以及一个包含所有 x 值的数组。y 数组被传递给尽可能多的数据属性,在series
组件中,用一个字符串在name
属性中指定它们的名称。相反,x 数组被传递给xAxis
中的categories
属性。在第二种方法中,直接定义包含 x 和 y 两个值的数组。由于这种选择比较费力,所以当系列在两个坐标上不共享相同的值时更好。然而,举例来说,如果你像清单 18-5 那样定义这些点,你会得到相同的结果。
清单 18-5。ch18_01c.html
var serie1 = [ [Date.UTC(2013, 3, 27, 9), 1],
[Date.UTC(2013, 3, 27, 11), 2],
[Date.UTC(2013, 3, 27, 13), 3],
[Date.UTC(2013, 3, 27, 15), 2],
[Date.UTC(2013, 3, 27, 17), 3],
[Date.UTC(2013, 3, 27, 19), 4] ]
var serie2 = [ [Date.UTC(2013, 3, 27, 9), 3],
[Date.UTC(2013, 3, 27, 11), 4],
[Date.UTC(2013, 3, 27, 13), 5],
[Date.UTC(2013, 3, 27, 15), 6],
[Date.UTC(2013, 3, 27, 17), 5],
[Date.UTC(2013, 3, 27, 19), 7] ];
var serie3 = [ [Date.UTC(2013, 3, 27, 9), 5],
[Date.UTC(2013, 3, 27, 11), 6],
[Date.UTC(2013, 3, 27, 13), 8],
[Date.UTC(2013, 3, 27, 15), 9],
[Date.UTC(2013, 3, 27, 17), 7],
[Date.UTC(2013, 3, 27, 19), 9] ];
var serie4 = [ [Date.UTC(2013, 3, 27, 9), 7],
[Date.UTC(2013, 3, 27, 11), 8],
[Date.UTC(2013, 3, 27, 13), 9],
[Date.UTC(2013, 3, 27, 15), 11],
[Date.UTC(2013, 3, 27, 17), 10],
[Date.UTC(2013, 3, 27, 19), 11] ];
$(function () {
$('#myChart').highcharts({
...
xAxis: {
title: {
text: 'Time',
x: 100
},
type: 'datetime',
//categories: ['9:00', '11:00', '13:00', '15:00', '17:00', '19:00'],
tickmarkPlacement: 'on'
},
...
});
});
当您必须处理datetime
数据:Date.UTC()
时,这个例子很好地介绍了在 Highcharts 中经常使用的一种方法。这是一个 JavaScript 函数,它给出了从 1970 年 1 月 1 日 0:00 到作为参数传递的日期之间的毫秒数。它可以接受的参数数量是可变的,这取决于日期应该有多精确:
Date.UTC( years, months, days, hours, minutes, seconds)
如果您只想指定一年中的某几天,只需编写前三个参数就足够了:
Date.UTC(2012, 4, 27)
如本例所示,如果您正在讨论小时数,那么您需要指定四个参数:
Date.UTC(2013, 3, 27, 11)
Note
月的范围是 0-11(不是 1-12),日的范围是 1-31 天(和往常一样),小时的范围是 0-23。这就是为什么在这个例子中四月用数字 3 来表示。
您已经看到了在 Highcharts 库中传递输入数据的两种方法。但是在下面的示例中,您将使用第三个选项,它允许您在 x 轴上定义一个数值范围,而无需定义任何类别数组(在我看来,这种选择更适合条形图,而不是折线图,但是现在处理这一点很重要,以便获得完整的情况)。事实上,在折线图中,x 轴上的值基于非常精确的刻度而增加。Highcharts 提供了一个名为plotOptions
,
的组件,它允许您定义两个特定的属性:pointInterval
和pointStart
。使用pointInterval
,您可以定义一个点和下一个点之间的间隔,使用pointStart,
您可以定义 x 轴上刻度的起始值。因为您必须处理 x 轴上一天的小时数,所以您有一个数据类型datetime
。因此,为了在不指定传递给categories
的值的情况下在 x 轴上获得相同的结果,您可以向配置对象添加新的组件,如清单 18-6 所示。
清单 18-6。ch18_01d.html
plotOptions: {
line: {
pointInterval: 2 * 3600 * 1000,// h * m* s hour
pointStart: Date.UTC(2013, 3, 27, 9, 0, 0)
}
},
网格:高级管理
使 Highcharts 成为比 jqPlot 更高级的库的特性之一是对图表底层网格的管理。事实上,这个库为您提供了一些属性,允许您在同一个图表上交替使用不同类型的网格布局,从而创造出令人愉悦的创新效果。通过修改你一直在做的例子,你会看到这是如何可能的(见清单 18-7)。
清单 18-7。ch18_01e.html
xAxis: {
title: {
text: 'Time',
x: 100
},
type: 'datetime',
gridLineWidth: 1,
gridLineDashStyle: 'dot',
minPadding: 0.1,
maxPadding: 0.1,
tickInterval: 4 * 3600 * 1000
},
yAxis: {
title: {
text: 'Visitors'
},
tickInterval: 4,
gridLineColor: '#618661',
minorTickInterval: 2,
minorGridLineColor: '#618661',
minorGridLineDashStyle: 'dashdot',
alternateGridColor: {
linearGradient: {
x1: 0, y1: 1,
x2: 1, y2: 1
},
stops : [
[0, "#F8F8EE"],
[1, "#A2B9A6"]
],
},
lineWidth: 1,
lineColor: '#CACACA',
tickWidth: 2,
tickLength: 4,
tickColor: '#CACACA'
},
除了不再用直线连接点,而是用曲线(对应于你在 jqPlot 中用smooth
得到的),你需要把图表的类型从line
改为spline
(见清单 18-8)。
清单 18-8。ch18_01e.html
chart: {
type: 'spline',
marginTop: 60,
marginLeft: 60
},
plotOptions: {
spline: {
pointInterval: 2 * 3600 * 1000,// h * m* s hour
pointStart: Date.UTC(2013, 3, 27, 9, 0, 0)
}
},
从这些变化中你会得到如图 18-4 所示的图表。
图 18-4。
A grid with alternating colors, a dash style, and a gradient effect
网格线有两种不同的属性,以grid-
和-minorGrid, and they
开始,可以分别在 x 轴和 y 轴上定义。这种分成两个不同类的方法允许您为每个轴定义两种不同类型的网格,这两种网格将在它们之间逐行交替,从而给出一个更丰富、更多样化的图表。
此外,对于在网格线之间定义的区域,可以应用交替的颜色,甚至是可以调整方向的渐变。这都要归功于alternateGridColor
属性,它包含两个要定义的附加属性。使用linearGradient
可以给渐变一个方向——0 和 1 指定 x 和 y 上渐变的两个极端。使用stops
,可以定义分配给 0 和 1 的颜色。
用 HTML 自定义工具提示
jqPlot 中什么构成了真正的定制,这里通过使用在tooltip
组件中定义的四个属性来形式化。useHTML
,如果设置为true
,通过 HTML 标签激活新工具提示结构的构造模式。使用headerFormat
、pointFormat
和footerFormat
属性,您可以分别定义工具提示的头部、主体和尾部。从一点到另一点变化的动态值可以通过将它们包含在 HTML 结构的大括号中来访问。使用{ series.color }
和{ series.name }
,您可以访问颜色和系列的名称,而使用{ point.x }
和{ point.y }
,您可以访问点的 x 和 y 值(见清单 18-9)。
清单 18-9。ch18_01e.html
tooltip: {
useHTML: true,
headerFormat: '<small>{point.key}</small><table>',
pointFormat: '<tr><td style="color: {series.color}">' +
'<img src="flags/{series.name}.png" '+
'height="22" width="32"> {series.name}:</td>' +
'<td style="text-align: right"><b>{point.y}</b></td></tr>',
footerFormat: '</table>'
},
Note
在工具提示中显示标志所需的 PNG 文件包含在源代码中,该源代码可在该书的 press 产品页面( www.apress.com/9781430262893
)的“源代码/下载”选项卡上找到。
图 18-5 显示了您刚才所做的结果。
图 18-5。
With the HTML customization of the tooltips, it is possible to modify their default layout
用 HTML 自定义图例
同样类型的定制也适用于图例,如图 18-6 所示。
图 18-6。
A legend, customized using HTML with images and colors
这里也有必要启用useHTML
属性,然后编写一个函数,将包含 HTML 代码的字符串返回给labelFormatter
属性(见清单 18-10)。
清单 18-10。ch18_01f.html
legend: {
layout: 'vertical',
align: 'right',
verticalAlign: 'top',
y: 60,
useHTML: true,
labelFormatter: function () {
return '<table><tr><td style="color: '+this.color +
'"><img src="flags/'+ this.name +
'.png" height="15" width="20"><b> ' +
this.name;+'</b></td></tr></table>'
}
},
添加波段
色带可用于突出显示或定义图表中的区域。该功能完全集成在plotBands
组件的 Highcharts 中,并且可以用很少的几行代码实现。该组件接受一个波段数组,您可以在每个波段中定义它的所有属性。根据定义plotBands
的位置——在 x 轴或 y 轴上——可以得到水平或垂直条带。from
和to
属性定义了波段的界限。使用color
属性,你可以决定用哪种颜色来绘制它。此外,借助label
属性,您可以在每个区带中添加文本引用。
这个例子展示了垂直带的使用,所以你需要在 x 轴上添加plotBands
组件,如清单 18-11 所示。
清单 18-11。ch18_01g.html
xAxis: {
...
plotBands: [{
from: Date.UTC(2013, 3, 27, 8, 0, 0),
to: Date.UTC(2013, 3, 27, 9, 0, 0),
color: '#CACACA',
label: {
text: 'Close',
style: {
color: '#000000'
}
}
},{
from: Date.UTC(2013, 3, 27, 19, 0, 0),
to: Date.UTC(2013, 3, 27, 20, 0, 0),
color: '#CACACA',
label: {
text: 'Close',
style: {
color: '#000000'
}
}
},{
from: Date.UTC(2013, 3, 27, 12, 30, 0),
to: Date.UTC(2013, 3, 27, 14, 0, 0),
color: '#FFE7B6',
label: {
text: 'Lunch',
style: {
color: '#000000'
}
}
}],
...
},
通过删除yAxis
组件中的alternateGridColor
属性及其所有内容,确保删除不再需要的交替区域网格的设置。清单 18-12 中加粗的代码行表明了这些变化。
清单 18-12。ch18_01g.html
yAxis: {
// You have to delete the following rows
alternateGridColor: {
linearGradient: {
x1: 0, y1: 1,
x2: 1, y2: 1
},
stops : [
[0, "#F8F8EE"],
[1, "#A2B9A6"]
],
},
//
},
此外,您可以进行进一步的定制。如果不喜欢默认颜色,可以用 jqPlot 中的颜色序列来替换它们。你将它们直接分配给colors
组件(见清单 18-13)。
清单 18-13。ch18_01g.html
$('#myChart').highcharts({
colors: ["#4bb2c5", "#c5b47f", "#EAA228", "#579575"],
chart: {
...
图 18-7 显示了结果。
图 18-7。
Delimiting some areas of the chart with colored bands can enrich it with further information
自定标记点
对于那些习惯使用 jqPlot 的人来说,Highcharts 提供了许多额外的特性。您已经看到了如何定制图例和工具提示。标志点是什么?你甚至可以影响这些图表组件上的许多操作。例如,假设您想要显示一个图标来代替其中一个标记点,以便第一眼就突出显示图形上该给定点的特定特征(参见代码清单后面的注释)。为了做到这一点,你可以直接在输入数组中指定适当的属性(见清单 18-14),精确地在你想要标记点的地方。
清单 18-14。ch18_01g.html
var series1 = [1, 2, 3, {y:2, marker:{ symbol:'url(icon/info.png)'}}, 3, 4];
var series2 = [3, 4, 5, 6, 5, 7];
var series3 = [5, 6, 8, 9, 7, 9];
var series4 = [7, 8, 9, 11, 10, 11];
Note
您可以在本书随附的源代码中找到图标图像info.png
,或者您也可以从 OpenIcon Library 网站的以下 URL 下载类似的图标: http://openiconlibrary.sourceforge.net
。这个网站包含了大量可供下载的图标。
输入数组中的每个值都可以被视为一个组件对象,由此可以指定所有属性,从而覆盖默认值。这种方法允许您在单个数据点级别区分行为和布局。
事实上,这将代替标记点出现(见图 18-8 )。
图 18-8。
Any icon can replace the original marker point, giving further info about a specific point in a series
排行榜的主题
在 jqPlot 中,您已经看到了通过定义级联样式表(CSS)类的属性来管理各种组件的样式,但是您还没有看到实际的主题。您可以考虑一个主题,比如 web 页面的各种组成元素上的一组特定的样式。这些配置存储在特殊文件中,以便可以重复使用。他们通常会创造出易于识别的个性化风格。这些配置通常被称为主题。
在其发行版中,Highcharts 提供了一些主题,您可以使用这些主题来描述您的图表:
- 格子
- 天空
- 灰色的
- 深蓝
- 深绿色
要在网站上设置这些主题,必须包含显示相同样式名称的 JS 文件。对于本地方法,请使用以下内容:
<script src="../src/js/themes/dark-blue.js" type="text/javascript"></script>
或者,如果您喜欢使用 CDN 服务:
<script src="
http://code.highcharts.com/themes/dark-blue.js
如图 18-9 所示,这些主题对你的图表布局的影响是显著的。
图 18-9。
In selecting a theme, you can accentuate certain aspects of your chart and increase the brightness of some colors
从文件中读取数据
通常,使用 Highcharts 或类似库的人需要读取文件中包含的数据。这一点非常重要,因为您的 web 页面及其 JavaScript 代码可以被视为一个真正的应用,它读取其他地方生成的数据,并且每次浏览器调用该页面时,这些数据都会发生变化。
生成数据的应用将数据写入可从网络访问的文件,如 CSV 文件。当用户想要查看图表中的数据时,浏览器会调用您编写 Highcharts 代码的 HTML 页面。每次都是 HTML 页面读取 CSV 文件(甚至可能不在同一台计算机上)中包含的数据,因此用户总是可以看到更新的数据。开发人员不需要每次对不同的页面重新编程,将数据写入 CSV 文件的应用将独立完成这项工作。当有一个数据库而不是一个文件时,就需要采取进一步的措施来请求数据。
使用$读取 CSV 文件。get()
为了读取文件的内容,jQuery 库提供了一个非常有用的函数:$.get()
。
$.get("aFile.html", function( data ) { ... });
这个函数使用 HTTP GET
请求从服务器加载文件,并允许您使用嵌套函数读取其内容。(有关更多信息,请参见位于 http://api.jquery.com/jQuery.get/
的 jQuery API 参考)。)
为了说明文件读取的过程,CSV 文件是一个很好的选择,因为这种格式很常见,使用起来也很简单。清单 18-15 显示了读取包含数据列的简单 CSV 文件的过程。
清单 18-15。data_08a.csv
date,open,min,max,close,
08/08/2012,1.238485,1.2227,1.250245,1.2372,
08/09/2012,1.23721,1.21671,1.24873,1.229295,
08/10/2012,1.2293,1.21417,1.25168,1.228975,
08/12/2012,1.229075,1.21747,1.23921,1.22747,
08/13/2012,1.227505,1.21608,1.24737,1.23262,
08/14/2012,1.23262,1.22167,1.248555,1.232385,
08/15/2012,1.232385,1.21641,1.254355,1.228865,
08/16/2012,1.22887,1.215625,1.247305,1.23573,
08/17/2012,1.23574,1.21891,1.23824,1.2333,
08/19/2012,1.23522,1.22291,1.245275,1.23323,
08/20/2012,1.233215,1.21954,1.256885,1.2351,
08/21/2012,1.23513,1.21465,1.258785,1.247655,
08/22/2012,1.247655,1.22315,1.264415,1.25338,
08/23/2012,1.25339,1.232465,1.288965,1.255995,
08/24/2012,1.255995,1.228175,1.276665,1.2512,
08/26/2012,1.25133,1.23042,1.292415,1.25054,
08/27/2012,1.25058,1.239025,1.28356,1.25012,
08/28/2012,1.250115,1.22656,1.287695,1.2571,
08/29/2012,1.25709,1.221895,1.29736,1.253065,
08/30/2012,1.253075,1.218785,1.27639,1.25097,
08/31/2012,1.25096,1.239375,1.283785,1.25795,
09/02/2012,1.257195,1.226845,1.298705,1.257355,
09/03/2012,1.25734,1.22604,1.271095,1.258635,
09/04/2012,1.25865,1.23264,1.282795,1.25339,
09/05/2012,1.2534,1.230195,1.27245,1.26005,
09/06/2012,1.26006,1.246165,1.28513,1.26309,
09/07/2012,1.26309,1.232655,1.291765,1.281625,
09/09/2012,1.28096,1.24915,1.311295,1.279565,
09/10/2012,1.27957,1.24552,1.30036,1.27617,
09/11/2012,1.27617,1.2459,1.29712,1.28515,
09/12/2012,1.28516,1.241625,1.31368,1.290235,
09/13/2012,1.227505,1.20608,1.25737,1.23262,
09/14/2012,1.24262,1.22167,1.278555,1.232385,
09/15/2012,1.252385,1.21641,1.284355,1.228865,
09/16/2012,1.24887,1.225625,1.257305,1.23573,
09/17/2012,1.24574,1.22891,1.26824,1.2333,
09/19/2012,1.24522,1.23291,1.255275,1.23323,
09/20/2012,1.233215,1.21954,1.256885,1.2351,
09/21/2012,1.22513,1.21465,1.248785,1.247655,
09/22/2012,1.227655,1.21315,1.254415,1.25338,
09/23/2012,1.22339,1.202465,1.258965,1.255995,
09/24/2012,1.215995,1.208175,1.256665,1.2512,
09/26/2012,1.22133,1.20042,1.252415,1.25054,
09/27/2012,1.22058,1.209025,1.25356,1.25012,
09/28/2012,1.230115,1.21656,1.257695,1.2571,
09/29/2012,1.24709,1.221895,1.25736,1.253065,
09/30/2012,1.233075,1.218785,1.25639,1.25097,
09/31/2012,1.24096,1.229375,1.263785,1.25795,
10/02/2012,1.257195,1.226845,1.258705,1.257355,
10/03/2012,1.25734,1.22604,1.271095,1.258635,
10/04/2012,1.25865,1.23264,1.282795,1.25339,
10/05/2012,1.2534,1.210195,1.28245,1.26005,
10/06/2012,1.26006,1.226165,1.28513,1.26309,
10/07/2012,1.26309,1.232655,1.281765,1.281625,
10/09/2012,1.28096,1.24915,1.291295,1.279565,
10/10/2012,1.29957,1.25552,1.31036,1.27617,
10/11/2012,1.30617,1.2559,1.32712,1.28515,
10/12/2012,1.28516,1.261625,1.31368,1.290235,
该文件包含五列。第一个是一列datetime
值,沿 x 轴标记时间,接下来的四个是 OHLC(开盘-盘高-盘低-收盘)数据类型。为了处理第一种类型的数据,您需要构建一个合适的解析器。这将包含一个regex
表达式来捕获第一列中的日、月和年的值,以及一个函数来以代码可以管理的格式重构它们(见清单 18-16)。
清单 18-16。ch18_02a.html
Highcharts.Data.prototype.dateFormats['m/d/Y'] = {
regex: '^([0-9]{1,2})\/([0-9]{1,2})\/([0-9]{4})$',
parser: function (match) {
return Date.UTC(match[3], match[1] - 1, +match[2]);
}
};
现在,您必须确保 JavaScript 代码能够读取 CSV 文件中包含的数据。为此,您必须声明函数$.get()
,并将文件名和扫描内容的函数作为参数传递。在$.get()
函数中,你放置了highcharts()
函数,这样它就可以通过csv
变量看到文件中包含的所有数据(见清单 18-17)。
清单 18-17。ch18_02a.html
$.get('data_08a.csv', function (csv) {
$('#myChart').highcharts(options);
});
现在是时候定义图表各个组件的所有属性了。就像在 jqPlot 中一样,即使使用 Highcharts,当考虑越来越复杂的情况时,最好还是在模块中推理。因此,从现在开始,您将通过options
变量在外部定义配置对象,然后将它作为参数传递给highcharts()
函数(参见清单 18-18)。
清单 18-18。ch18_02a.html
$.get('data_08.csv', function (csv) {
var options = {
colors: ["#005B06", "#000000", "#9D3C27", "#000000"],
data: {
csv: csv
},
subtitle: {
text: 'Prices of the day',
style: {
color: '#005B06',
fontSize: '12px'
}
},
title: {
text: 'Spaghetti Lunghetti',
style: {
color: '#005B06',
fontSize: '16px'
}
},
xAxis: {
type: 'datetime',
tickInterval: 7 * 24 * 3600 * 1000, // one week
tickWidth: 0,
gridLineWidth: 1,
labels: {
align: 'left'
}
},
yAxis: {
title: {
text: 'Dollars ($)',
style: {
color: '#005B06',
fontSize: '12px'
}
}
},
plotOptions: {
area: {
fillColor: {
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1},
stops: [ [0, '#00602F'], [1, '#FFFFFF']]
},
lineWidth: 1,
marker: {
enabled: false
},
shadow: false,
states: {
hover: { lineWidth: 1 }
},
threshold: null
},
},
series: [{
name: 'Open',
type: 'area',
lineWidth: 4,
marker: {
radius: 4
},
},{ visible: false},
{
name: 'Max',
type: 'spline',
lineWidth: 4,
marker: {
enabled: false
}
},{ visible: false}]
};
$('#myChart').highcharts(options);
});
要激活读取写入文件的数据,您需要包含data
模块,您可以在发行版中找到它。对于本地方法,请使用以下内容:
<script src="../src/js/modules/data.js"></script>
或者,如果您喜欢使用 CDN 服务:
<script src="
http://code.highcharts.com/modules/data.js
在浏览器中加载页面,你会得到图 18-10 中的图表。
图 18-10。
By default all the series in an imported CSV file are read; you can hide them if need be
从数据中排除 CSV 列
请注意,CSV 文件包含占据最后四列数据的 OHLC 值列表。假设在这种情况下,您只对显示那些包含open
和max
值的列感兴趣。你想忽略剩下的两个。如果查看series
对象,还需要定义四个不同的数列,其中第二个和第四个隐藏在图表中(没有删除!).因此,min
和close
系列仍然保留在图例中。如果你点击它们,这些系列会出现在图表上,很多时候这是不可取的。
当您想只显示第一列而不显示最后一列时,您必须采用不同的方法(例如,如果您想只显示open
,或open
和min
,或open
、min
和max
)。为此,您需要在series
对象中分别指定第一列、前两列或前三列。但是你不能跳过一个系列直接考虑下一个。
因此,如果您打算查看文件中的所有系列,这种方法是最佳的,但有时情况并非如此。您经常会有包含大量列的文件,但希望只提取几列。然后,您需要遵循不同的方法,例如,输入一个 JavaScript 函数作为解析器,只提取您感兴趣的列。现在,您将看到如何修改前面的示例来实现这一点。
首先,在函数$.get()
读取了 CSV 文件中包含的所有数据后,立即插入一个解析器。您定义了两个包含数据列open
和max
的数组,它们都与日期相关。随后通过逐行扫描,这两个数组将填充相应的值。这样,您排除了您不感兴趣的列,并且您可以只处理open
和max
变量,它们将被传递给series
对象。清单 18-19 显示了这方面的代码。
清单 18-19。ch18_02b.html
$.get('data_08a.csv', function (csv) {
var open = [];
var max = [];
var lines = csv.split('\n');
$.each(lines, function(lineNo, line) {
var items = line.split(',');
if (lineNo != 0) {
open.push([items[0], parseFloat(items[1])]);
max.push([items[0], parseFloat(items[3])]);
}
});
...
options
中的data
对象(见清单 18-20)不再需要;因此,您应该删除它。
清单 18-20。ch18_02b.html
var options = {
colors: ["#005B06", "#9D3C27"],
//data: {
//csv: csv
//},
使用data
属性将这两个数组直接传递给series
对象(见清单 18-21)。
清单 18-21。ch18_02b.html
series: [{
data: open,
name: 'Open',
type: 'area',
lineWidth: 4,
marker: {
radius: 4
},
},{
data: max,
name: 'Max',
type: 'spline',
lineWidth: 4,
marker: {
enabled: false
}
}]
图 18-11 显示了不再引用不需要的系列的新图表。
图 18-11。
By adding a parser, it is possible to filter only for series of interest (look at the legend)
导出图表
Highcharts 提供的一个全新特性是用户可以从浏览器中导出各种格式的图表,包括 PNG 或 JPG 图像、SVG 矢量或 PDF 文档。甚至可以打印图像。所有这些都是通过使用图表右上角的小按钮访问的菜单来完成的(参见图 18-12 )。
图 18-12。
By clicking the icon in the upper-right corner of the chart, you’ll access a context menu that contains options for exporting the chart
只要您在网页中包含exporting
模块,该按钮就会出现在您的图表中。您可以在 Highcharts 发行版中找到这个模块。对于本地方法,请使用以下内容:
<script src="../src/js/modules/exporting.js"></script>
或者,如果您喜欢使用 CDN 服务:
<script src="
http://code.highcharts.com/modules/exporting.js
主详细信息图表
主详细信息图表就是一个折线图的例子,它恰当地说明了像 Highcharts 这样的库所能提供的功能。这种类型的折线图由同时显示的同一折线图的两种表示形式组成。目的是将焦点集中到折线图的特定部分,而不会丢失整个图表。
这两个图表称为主图表和详细图表。正如您在图 18-13 中看到的,主图表通常要小得多,并且被放置在相对于细节图表的边缘位置;它通常显示在底部,其 x 轴与详细信息图表对齐,以提供透视感。详细信息图表位于前台,以便用户可以关注它。详细图表更详细地显示主图表的选定区域。
图 18-13。
A master and detail chart
当您必须处理大量难以完整查看的数据时,主详细信息图表是一个不错的选择。这些数据通常被写入从其他应用获取的外部文件中。继续前面的意大利面条价格示例,您将从一个 CSV 文件构建一个主细节图表。
在开始真正的例子之前,您需要在本地方法中包含库文件和深绿色主题:
<script type="text/javascript" src="../src/js/jquery-1.9.1.js"></script>
<script type="text/javascript" src="../src/js/highcharts.js"></script>
<script type="text/javascript" src="../src/js/modules/data.js"></script>
<script type="text/javascript" src="../src/js/themes/dark-green.js"></script>
或者,如果您喜欢使用 CDN 服务:
<script src="
http://code.jquery.com/jquery-1.9.1.min.js
<script src="
http://code.highcharts.com/highcharts.js
<script src="
http://code.highcharts.com/modules/data.js
<script src="
http://code.highcharts.com/themes/dark-green.js
因为在这个例子中,你将使用与前一个例子相同的 CSV 文件(见清单 18-15),你需要一个合适的解析器来正确地读取数据。这个解析器将包含一个regex
表达式来捕获日、月和年的值。清单 18-22 展示了您之前重新开发的解析器(见清单 18-16),因为它仍然有效。
清单 18-22。ch18_03.html
<script>
$(function () {
Highcharts.Data.prototype.dateFormats['m/d/Y'] = {
regex: '^([0-9]{1,2})\/([0-9]{1,2})\/([0-9]{4})$',
parser: function (match) {
return Date.UTC(match[3], match[1] - 1, +match[2]);
}
};
});
</script>
...
<div id="myChart" style="width: 600px; height: 400px;"></div>
别忘了添加一个以myChart
为id
的<div>
元素。
主细节图由两个折线图组成,您可以通过两个变量来定义它们:masterChart
和detailChart
(参见清单 18-23)。这两张图表将绘制在两个不同的区域,master-container
和detail-container
。多亏了 jQuery,您将使用这些标识符创建两个<div>
元素。这两个图表将由两个 JavaScript 函数创建,createDetail()
和createMaster()
。甚至在控制两个图表的布局和行为的两个配置对象的定义中也保持了这种二元性:detailOptions
和masterOptions
。
清单 18-23。ch18_03.html
$.get('data_08a.csv', function (csv) {
var masterChart,detailChart;
function createMaster() {
masterChart = $('#master-container')
.highcharts(masterOptions, function(masterChart) {
createDetail(masterChart);
}).highcharts(); // return chart instance
}
function createDetail(masterChart) {
var detailData = [],
detailStart = Date.UTC(2012, 7, 1);
jQuery.each(masterChart.series[0].data, function(i, point) {
if (point.x >= detailStart) {
detailData.push(point.y);
}
});
detailChart = $('#detail-container').highcharts(detailOptions)
.highcharts();
}
var $container = $('#myChart')
.css('position', 'relative');
var $detailContainer = $('<div id="detail-container">')
.appendTo($container);
var $masterContainer = $('<div id="master-container">')
.css({ position: 'absolute', top: 300, height: 80, width: '100%'})
.appendTo($container);
createMaster();
});
每次重新绘制图表时,首先调用createDetail()
函数。它会创建相应的图表。在函数内部,调用了createMaster()
函数,该函数基于第一个图表中的选定区域创建第二个图表。
现在,您必须定义两个配置对象。在清单 18-24 中,您定义了关于主图表的选项。当查看masterOptions
对象时,请将注意力集中在chart
对象中events
属性的定义上。如您所见,在selection
属性中定义了一个通用函数。事实上,在主图表中,您需要用鼠标选择一个特定的范围。因此,该图表部分将显示在详细图表中。通过这个函数实现的恰恰是这个特性。extremesObject
变量存储所选区域覆盖的 x 轴的间隔,并将其传递给event.xAxis[0]
值。因此,您提取这个范围的最大值和最小值(min
和max
),这正是您需要的极端值。然后,这些值既用于绘制详细图表中的点,也用于绘制主图表中的阴影区域。事实上,下一步是使用一个each()
函数来选择min
和max
值之间的数据。所有这些数据点都存储在detailData
数组中。该数组将成为详细信息图表的输入数据数组。
清单 18-24。ch18_03.html
var masterOptions = {
colors: ['#FFE76D'],
data: {
csv: csv
},
chart: {
reflow: false,
borderWidth: 0,
backgroundColor: null,
marginLeft: 50,
marginRight: 20,
zoomType: 'x',
events: {
selection: function(event) {
var extremesObject = event.xAxis[0],
min = extremesObject.min,
max = extremesObject.max,
detailData = [],
xAxis = this.xAxis[0];
jQuery.each(this.series[0].data, function(i, point) {
if (point.x > min && point.x < max) {
detailData.push({
x: point.x,
y: point.y
});
}
});
xAxis.removePlotBand('mask-before');
xAxis.addPlotBand({
id: 'mask-before',
from: Date.UTC(2012, 1, 1),
to: min,
color: 'rgba(1, 1, 1, 0.5)'
});
xAxis.removePlotBand('mask-after');
xAxis.addPlotBand({
id: 'mask-after',
from: max,
to: Date.UTC(2013, 1, 1),
color: 'rgba(1, 1, 1, 0.5)'
});
detailChart.series[0].setData(detailData);
return false;
}
}
},
title: {
text: null
},
xAxis: {
type: 'datetime',
showLastTickLabel: true,
title: {
text: null
}
},
yAxis: {
gridLineWidth: 0,
labels: {
enabled: false
},
title: {
text: null
},
min: 1.25,
max: 1.31
},
tooltip: {
formatter: function() {
return false;
}
},
legend: {
enabled: false
},
credits: {
enabled: false
},
plotOptions: {
series: {
fillColor: {
linearGradient: [0, 0, 0, 70],
stops: [
[0, '#FFE76D'],
[1, 'rgba(0, 0, 0, 0)']
]
},
lineWidth: 1,
marker: {
enabled: false
},
shadow: false,
states: {
hover: {
lineWidth: 1
}
},
enableMouseTracking: false
}
},
series: [{
name: 'open',
type: 'area',
lineWidth: 2
}],
exporting: {
enabled: false
}
};
min
和max
值的另一个用途是界定主图表中的选定区域。您可以通过添加两个波段并用mask-before
和mask-after
id 标识它们来实现这一点。这些带是用来给没有被选中的区域加阴影的。因此,第一个波段将从实验数据的最低可能值开始(超出主图表的左边缘)。因此,例如,您将值Date.UTC (2012,1,1)
赋给from
属性,波段将对应于赋给to
属性的min
值。同样的事情也适用于第二个波段。它从分配给from
属性的max
值开始,以实验数据的最高可能值结束(超出主图表的右边缘)。您可以将Date.UTC(2013,1,1)
分配给to
属性。
在清单 18-25 中,您定义了细节图表的选项。如您所见,这个定义与折线图的定义没有太大的不同。
清单 18-25。ch18_03.html
var detailOptions = {
colors: ["#FFE76D"],
data: {
csv: csv
},
chart: {
marginBottom: 120,
reflow: false,
marginLeft: 50,
marginRight: 20,
style: {
position: 'absolute'
}
},
credits: {
enabled: false
},
title: {
text: 'Spaghetti Lunghetti',
style:{
color: '#FFE76D',
fontSize: '16px'
}
},
xAxis: {
type: 'datetime'
},
yAxis: {
title: {
text: null
},
maxZoom: 0.1
},
tooltip: {
formatter: function() {
var point = this.points[0];
return Highcharts.dateFormat('%A %B %e %Y', this.x) + ':<br/><b>'+
Highcharts.numberFormat(point.y, 2) +' USD</b>';
},
shared: true
},
legend: {
enabled: false
},
plotOptions: {
series: {
marker: {
enabled: false,
states: {
hover: {
enabled: true,
radius: 3
}
}
}
}
},
series: [{
name: 'Open',
lineWidth: 4
}],
exporting: {
enabled: false
}
};
最后,这里是主细节图(见图 18-14 )。
图 18-14。
The master chart is on the bottom and the detail chart is in the foreground
从主图表中,你可以用鼠标选择你感兴趣的部分,你将能够看到它的细节。
带高图表的条形图和饼图
在本节中,您将研究三个简短的示例,展示 Highcharts 如何实现常见的条形图和饼图。
事实上,你会看到实现一个条形图是多么简单。借助一些示例,您将看到如何更轻松地在分组模式和堆叠模式之间进行切换,从水平模式切换到垂直模式。甚至实现一个饼图也是非常简单和直观的。这一点很重要,因为正如您将在随后的章节中看到的,条形图和饼图本身被用作更复杂图表类型的组件。
条形图
由于条形图易于实现,您将立即从堆叠的垂直条形图开始。首先,记得包含库文件和一个主题,比如这里包含的深绿色主题:
<script type="text/javascript" src="../src/js/jquery-1.9.1.js"></script>
<script type="text/javascript" src="../src/js/highcharts.js"></script>
<script type="text/javascript" src="../src/js/themes/dark-green.js"></script>
或者,如果您喜欢使用 CDN 服务:
<script type="text/javascript" src="
http://code.jquery.com
code.jquery.com/jquery-1.9.1.min.js
<script type="text/javascript"
src="
http://code.highcharts.com/3.0.5/highcharts.js
<script type="text/javascript" src="
http://code.highcharts.com/themes
code.highcharts.com/themes/dark-green.js
您将看到,只需更改一个属性,就可以得到各种条形图。然后,为了创建一个带有竖条的条形图,有必要将type
属性指定为column
(见清单 18-26)。
清单 18-26。ch18_04a.html
$(function () {
var data1 = [46.6, 14.8, 0, 61.6];
var data2 = [2.6, 13.8, 72.6, 9.1];
var data3 = [3.3, 53.5, 77.1, 10.6];
var options = {
chart: {
type: 'column'
},
title: {
text: 'Nutrition label'
},
xAxis: {
categories: ['Carrots', 'Beans', 'Chicken', 'Bread']
},
yAxis: {
min: 0,
title: {
text: 'Calories'
}
},
legend: {
reversed: true
},
plotOptions: {
series: {
stacking: 'normal'
}
},
series: [{
name: 'Carbohydrate',
data: data1
},{
name: 'Fat',
data: data2
},{
name: 'Protein',
data: data3
}]
}
$('#myChart').highcharts(options);
});
图 18-15 显示了我们的图表。
图 18-15。
A simple stacked bar chart
如果你想要一个水平堆积条形图,你必须将type
属性定义为bar
,如清单 18-27 所示。
清单 18-27。ch18_04b.html
chart: {
type: 'bar'
},
这是从垂直转换为水平的图表(见图 18-16 )。
图 18-16。
A simple horizontal stacked bar chart
如果你想要一个普通的分组条形图,你只需通过options
删除plotOptions
对象,如清单 18-28 所示。
清单 18-28。ch18_04c.html
//Delete the plotOptions object
plotOptions: {
series: {
stacking: 'normal'
}
},
您将得到两个分组条形图,如图 18-17 所示。
图 18-17。
The previous bar charts in the grouped version
饼图
用 Highcharts 库构建一个饼图也非常简单(见清单 18-29)。Highcharts 接受格式为[label,value]的传入数据,然后将值转换为百分比,并根据百分比以正确的比例创建切片。
清单 18-29。ch18_04d.html
$(function () {
var data2 = [['Analysis', 5], ['Designing', 10], ['Developing', 20],
['Deploying', 5], ['Test', 28], ['Debugging', 23], ['Sale', 9]];
//Add options here
$('#myChart').highcharts(options);
});
看看在plotOptions
对象中的pie
对象中指定的属性。通过将allowPointSelect
属性设置为true
并将cursor
属性设置为pointer
(参见清单 18-30),您可以管理特定的事件。当用户点击一个切片时,它被从蛋糕中提取出来并到达在slicedOffset
属性中指定的距离。
清单 18-30。ch18_04d.html
var options = {
colors: ['#65Af43', '#FFE76D', '#BB43F2',
'#A50f33', '#15CACA', '#612BF3', '#FF8E04'],
chart: {
},
title: {
text: 'Developing of the X Weapon'
},
yAxis: {
min: 0,
title: {
text: 'Calories'
}
},
tooltip: {
pointFormat: '{series.name}: <b>{point.y}%</b>',
},
plotOptions: {
pie: {
allowPointSelect: true,
cursor: 'pointer',
showInLegend: true,
slicedOffset: 20
}
},
series: [{
type: 'pie',
name: 'Budget',
data: data2,
borderColor: '#888888',
borderWidth: 1,
dataLabels: {
enabled: true,
color: '#bbbbbb',
connectorColor: '#bbbbbb',
format: '{point.name}: <b>{point.y}%</div></b>'
},
}]
}
图 18-18 显示了使用深绿色主题的饼图。
图 18-18。
A pie chart with all elements included
甘特图
另一种可以使用 Highcharts 轻松创建的图表是甘特图(见图 18-19 )。这是一种条形图,由亨利·甘特于 1910 年开发,用于项目进度安排。最近,它被用来表示活动之间的关系。例如,在关于 Java 或 C++高级编程的书籍中,您通常会发现这样的图表表示各种函数调用之间的同步性或非同步性。
图 18-19。
A Gantt chart representing the development process of a project
要实现这种类型的图表,您必须包括扩展模块highcharts-more.js
。这个扩展包含了最近开发的许多类型的组件,包括columrange
类型,它允许您实现条形图的经典条,但是不同之处在于定义了最大值(高)和最小值(低)。因此,用水平方向(x 轴)和垂直方向(y 轴)来表示一个范围是很有用的。甘特图在 x 轴上描述时间间隔,而在 y 轴上需要分组分类。这种分类可以通过向categories
属性传递一个名称数组来轻松完成。
在下面的例子中,您将使用灰色主题。在实现图表之前,您需要在网页上包含这两个文件:
<script type="text/javascript" src="../src/js/highcharts-more.js"></script>
<script type="text/javascript” src="../src/js/themes/gray.js"></script>
或者,如果您喜欢使用 CDN 服务:
<script src="
http://code.highcharts.com/highcharts-more.js
<script src="
http://code.highcharts.com/themes/gray.js
请记住,甘特图表示时间范围,因此您可以通过每个元素有两个值的数组来定义时间范围:[低,高]。因为它们是datetime
值,建议选择使用函数Date.UTC()
来定义这些时间间隔(见清单 18-31)。
清单 18-31。ch18_05.html
var data1 = [ [Date.UTC(2012, 0, 1), Date.UTC(2012, 1, 15)],
[Date.UTC(2012, 0, 20), Date.UTC(2012, 1, 28)],
[Date.UTC(2012, 1, 4), Date.UTC(2012, 3, 30)],
[Date.UTC(2012, 2, 10), Date.UTC(2012, 5, 15)],
[Date.UTC(2012, 4, 1), Date.UTC(2012, 7, 19)],
[Date.UTC(2012, 6, 1), Date.UTC(2012, 10, 15)],
[Date.UTC(2012, 9, 1), Date.UTC(2012, 11, 28)];
现在你用options
变量定义配置对象(见清单 18-32)。在chart
组件对象中,需要将inverted
属性设置为true
。这是因为您希望条是水平的。但是,你必须小心,因为这样做的话,你就颠倒了轴。垂直轴是 x 轴,水平轴是 y 轴。当您定义每个属性时,尤其是当您需要弄清楚如何正确输入数据时,您需要考虑这一点。
清单 18-32。ch18_05.html
var options = {
chart: {
type: 'columnrange',
inverted: true
},
这种颠倒的第一个后果是,您必须在xAxis
对象中输入类别的名称,以便它们出现在垂直轴上。在图表中输入一个标题和副标题后,在categories
属性中插入一个字符串数组,包含分配给每个时间间隔的名称(见清单 18-33)。
清单 18-33。ch18_05.html
title: {
text: 'Developing of the X Weapon'
},
subtitle: {
text: 'Half Guns inc.'
},
xAxis: {
categories: ['Analysis', 'Designing', 'Developing',
'Deploying', 'Test', 'Debugging', 'Sale']
},
...
另一方面,您必须将 y 轴视为时间轴,并相应地定义网格。使用title
属性定义轴标签,并通过在type
属性中指定datetime
来定义轴必须管理的数据类型。然后你必须将minPadding
和maxPadding
属性设置为0
。这将迫使图表用输入数据中定义的时间间隔填充整个 x 轴(见清单 18-34)。
清单 18-34。ch18_05.html
yAxis: {
title: {
text: 'Scheduling'
},
type: 'datetime',
minPadding: 0,
maxPadding: 0,
gridLineWidth: 1,
gridLineColor: '#bbbbbb',
minorTickInterval: 14 * 24 * 3600000 //2 week
},
...
您已经告诉 Highcharts 库通过chart
对象中的type
属性将数据解释为columnrange
。现在是时候定义它的属性了,你可以在series
对象中定义,也可以在plotOptions
对象下的columnrange
对象中定义(见清单 18-35)。除非另外指定,否则您将拥有相同颜色的所有条形,因为它们属于同一系列,但是因为您想要相反的颜色,所以您必须用true
激活colorByPoint
属性,然后将所需的颜色序列指定为colors
属性的数组。最后,在禁用图例和工具提示之后,将数据传递给series
对象中的data
属性。
清单 18-35。ch18_05.html
...
plotOptions:{
columnrange:{
colorByPoint: true,
colors:['#65Af43','#FFE76D','#BB43F2','#A50f33',
'#15CACA','#612BF3','#FF8E04']
}
},
legend: {
enabled: false
},
tooltip: {
enabled: false
},
series: [{
data: data1,
borderColor: 'black',
borderWidth: 2
}]
}
$('#myChart').highcharts(options);
最后,这是我们的甘特图(见图 18-20 )。
图 18-20。
A Gantt chart
组合图表
或许 Highcharts 库与众不同的一面是,它能够在同一个图表中重叠几种类型的图表,从而创建真正壮观的表示。此外,该库的结构使得叠加图表的配置值保持不同,因此易于管理。您可以分别开发它们,然后最终将它们组合在一起。
在下一个示例中,您将连接已经实现的饼图和甘特图,并添加第三个图表,即折线图。首先,导入所有必要的文件,包括灰色主题:
<script type="text/javascript" src="../src/js/jquery-1.9.1.js"></script>
<script type="text/javascript" src="../src/js/highcharts.js"></script>
<script type="text/javascript" src="../src/js/highcharts-more.js"></script>
<script type="text/javascript" src="../src/js/themes/gray.js"></script>
或者,如果您喜欢使用 CDN 服务:
<script src="
http://code.highcharts.com/jquery-1.9.1.js
<script src="
http://code.highcharts.com/highcharts.js
<script src="
http://code.highcharts.com/highcharts-more.js
<script src="
http://code.highcharts.com/themes/gray.js
您需要定义将用于所有三个图表的数据(见清单 18-36)。在data1,
中,输入定义甘特图间隔的数据【低,高】。在data2,
中,您输入饼图的数据。注意,为了方便起见,这些值已经表示为百分比。在data3
中,您输入代表折线图各点的[x,y]数据(此处您将datetime
值定义为 y 值,将数值定义为 x 值,因为您已经反转了轴)。最后,在gradient
变量中添加颜色渐变的定义,以避免在配置对象中多次写入。
清单 18-36。ch18_06.html
var data1 = [[Date.UTC(2012, 0, 1), Date.UTC(2012, 1, 15)],
[Date.UTC(2012, 0, 20), Date.UTC(2012, 1, 28)],
[Date.UTC(2012, 1, 4), Date.UTC(2012, 3, 30)],
[Date.UTC(2012, 2, 10), Date.UTC(2012, 5, 15)],
[Date.UTC(2012, 4, 1), Date.UTC(2012, 7, 19)],
[Date.UTC(2012, 6, 1), Date.UTC(2012, 10, 15)],
[Date.UTC(2012, 9, 1), Date.UTC(2012, 11, 28)]];
var data2 = [5, 10, 20, 5, 28, 23, 9];
var data3 = [[140, Date.UTC(2012,0,1)],
[120, Date.UTC(2012, 1, 28)],
[58, Date.UTC(2012, 3, 30)],
[78, Date.UTC(2012, 5, 15)],
[44, Date.UTC(2012, 7, 19)],
[33, Date.UTC(2012, 10, 15)],
[1, Date.UTC(2012, 11, 28)]];
var gradient = {x1:0, y1:0, x2:0, y2:1};
现在可以开始定义配置对象了(见清单 18-37)。因为上一个例子中的颜色序列效果很好,所以您将在这个例子中再次使用它。然后使用true
激活inverted
属性来反转 x 轴和 y 轴。最后,给你的图表添加一个标题和副标题。
清单 18-37。ch18_06.html
var options = {
colors:['#65Af43', '#FFE76D', '#BB43F2',
'#A50f33', '#15CACA', '#612BF3', '#FF8E04'],
chart: {
inverted: true
},
title: {
text: 'Developing of the X Weapon'
},
subtitle:{
text: 'Half Guns inc.'
},
对于组合图,你需要在 x 轴上定义两个不同的刻度——一个包含上例中的类别,另一个是线性类型,代表线性图(见清单 18-38)。关于 y 轴,您必须定义一个时间刻度,并且还要管理网格。y 轴上刻度后面的主网格扫描月份,而您将设置次网格扫描两周的时间段。
清单 18-38。ch18_06.html
xAxis:[ {
categories: ['Analysis', 'Designing', 'Casting',
'Develop', 'Test', 'Debugging', 'Sale']
},{
title: {
text: 'Budget'
},
labels: {
enabled: false
},
opposite: true,
tickInterval: 20
}],
yAxis: {
title: {
text: 'Scheduling'
},
type: 'datetime',
minPadding: 0,
maxPadding: 0,
gridLineWidth: 1,
gridLineColor: '#bbbbbb',
minorTickInterval: 14 * 24 * 3600000 //2 week
},
对于甘特图,你需要对之前的版本做一些修改(见清单 18-39)。将颜色渐变应用到条形和区域。在这里,您可以使用渐变透明度来创建动态外观。
清单 18-39。ch18_06.html
plotOptions:{
columnrange:{
colorByPoint: true,
colors: [{
linearGradient: gradient,
stops: [[0, 'rgba(101, 175, 67, 1)'],
[1, 'rgba(101, 175, 67, 0)']]
},{
linearGradient: gradient,
stops: [[0, 'rgba(255, 231, 109, 1)'],
[1, 'rgba(255, 231, 109, 0)']]
},{
linearGradient: gradient,
stops: [[0, 'rgba(187, 67, 242, 1)'],
[1, 'rgba(187, 67, 242, 0)']]
},{
linearGradient: gradient,
stops: [[0, 'rgba(165, 15, 51, 1)'],
[1, 'rgba(165, 15, 51, 0)']]
},{
linearGradient: gradient,
stops: [[0, 'rgba(21, 202, 202, 1)'],
[1, 'rgba(21, 202, 202, 0)']]
},{
linearGradient: gradient,
stops: [[0, 'rgba(97, 43, 243, 1)'],
[1,'rgba(97, 43, 243, 0)']]
},{
linearGradient: gradient,
stops: [[0, 'rgba(255, 142, 4, 1)'],
[1, 'rgba(255, 142, 4, 0)']]
}]
}
},
您还可以禁用图例和工具提示,并开始输入三种图表类型的属性。在series
对象中,您定义了三个不同的对象,每个对象对应一个图表。当您使用组合图表时,您必须记住它们的绘制顺序。事实上,在series
对象中定义的每个图表都是在前一个图表之上绘制的。如果有重叠的部分,它们将被覆盖。所以在series
对象中定义图表的顺序很重要。
首先,定义折线图,以使其成为背景。在本例中,您还可以修改标记的形状、大小和颜色。然后定义甘特图,记住将值0
添加到属性xAxis
中。这意味着甘特图中的数据,即data1
,被传递到第一个 x 轴。最后,定义饼图的属性,使用center
属性可以将饼图移动到不与其他图表重叠的位置(见清单 18-40)。使用size
属性,您可以减小馅饼的大小,以便更好地适应您的需求。与 jqPlot 不同,Highcharts 包含连接符,连接符是将标签连接到相应切片的线。
清单 18-40。ch18_06.html
legend: {
enabled: false
},
tooltip: {
enabled: false
},
series: [{
type: 'line',
data: data3,
xAxis:1,
color: ['#ddddaa'],
lineWidth: 4,
dashStyle: 'dash',
marker: {
fillColor: 'rgba(0,0,0,1)',
lineWidth: 4,
lineColor: '#ddddaa',
radius: 7
}
},{
type: 'columnrange',
data: data1,
borderWidth: 0,
xAxis:0
},{
type: 'pie',
name: 'Budget',
data: data2,
borderColor: '#888888',
borderWidth: 1,
center: [370, 40],
size: 100,
showInLegend: false,
dataLabels: {
enabled: true,
color: '#bbbbbb',
connectorColor: '#bbbbbb',
formatter: function() {
return '<b>'+ Math.round(this.percentage) +'%</b>';
}
},
xAxis:1
}]
}
$('#myChart').highcharts(options);
完成后的组合图如图 18-21 所示。
图 18-21。
A combined chart with three different types of chart
高库存图书馆
当您访问 Highcharts 的官方网站时,除了这个库,您还会发现指向另一个名为 Highstock 的库的指示。该库完全集成到 Highcharts 中,并提供了一系列将在您的图表中进一步引入的功能和工具。它允许您创建非常高级的导航工具,将您的图表转换为真正的工具,用于对覆盖很长时间的大量数据进行专业分析。
如果你对图表开发的兴趣导致你面对非常复杂的专业需求,特别是在经济分析方面,这个库将被证明是一个不可或缺的工具。因为它的内容主要包括集成到现有图表中的工具以及它们各自对特定需求的高度特异性,所以本书不讨论这个主题。不过,我强烈建议您通过官方网站上提供的各种演示来看看它的内容( www.highcharts.com/products/highstock
)。在这方面也存在大量的文档(参见 http://api.highcharts.com/highstock
的 API Highstock 参考)。
摘要
本章涵盖了 Highcharts 库,是本书第二部分的结尾。您已经看到这个库如何继承了 jqPlot 的所有基本特性,并将其扩展到更专业的水平。
通过实现一个简单的折线图的基础,本章比较了两个库,突出了相同点和不同点。然后,您看到了如何在 Highcharts 打开的情况下处理组件的定制,以及如何从文件中读取数据。
有了 Highcharts,你就实现了其他类型的图表,比如甘特图、主图和细节图,这两种图表都强调了这个库所提供的增加的开发可能性。
最后,再次以条形图和饼图为例,您了解了 Highcharts 如何允许您在同一绘图区域中同时组合几种不同类型的图表,进一步扩展了开发更具创新性和引人注目的图表的可能性。
下一章以讨论 D3 库开始了本书的第三部分,也是最后一部分。它介绍了该库的所有基本概念,并提供了一些基本的入门示例。您会发现它是 jqPlot、Highcharts 和其他基于 jQuery 和 Canvas 的类似库的替代品。您将看到这个库如何使用 SVG 技术创建构建图表所需的所有图形元素,就像它们是小砖块一样。最后,你将分析到目前为止你所看到的一切的优势和差异。
十九、使用 D3
Abstract
这一章是本书第三部分的开始,关于 D3 库。这个库在书中有一个单独的章节专门介绍它,因为它在许多方面不同于 jqPlot 和 Highcharts 库。在本章的各个部分,以及在接下来的章节中,当你更深入地研究这个库的各个方面时,你将能够体会到 D3 有一个独特的和创新的结构。首先,它没有使用 jQuery,但是它再现了数据可视化所必需的所有特性。在 jqPlot 和 Highcharts 库中,已经创建了图表组件,只需要用户通过 options 对象来调整它们的属性,而 D3 实际上采用了相反的方法。
这一章是本书第三部分的开始,关于 D3 库。这个库在书中有一个单独的章节专门介绍它,因为它在许多方面不同于 jqPlot 和 Highcharts 库。在本章的各个部分,以及在接下来的章节中,当你更深入地研究这个库的各个方面时,你将能够体会到 D3 有一个独特的和创新的结构。首先,它没有使用 jQuery,但是它再现了数据可视化所必需的所有特性。在 jqPlot 和 Highcharts 库中,已经创建了图表组件,只需要用户通过 options 对象来调整它们的属性,而 D3 实际上采用了相反的方法。
D3 库允许您构建任何表示,从最基本的图形元素开始,比如圆、线、正方形等等。当然,这种方法会使图表的实现变得非常复杂,但同时,它允许您开发全新的图形表示,而不必遵循其他图形库提供的预设模式。
因此,在本章的过程中,你将熟悉这个库的基本概念。您将看到它们中的一些——比如选择、选择器和方法链——是如何从 jQuery 库中提取的。您还将了解如何操作各种文档对象模型(DOM)元素,尤其是可伸缩矢量图形(SVG)元素的创建,它们是图形表示的基本构件。
本章最后简要介绍了 SVG 元素的变换和转换。
你将从介绍这个奇妙的图书馆开始。
FIREBUG: DEBUGGING D3 CODE
在开始一些实际例子之前,我想提醒您使用 FireBug 进行调试。至少,一定要有一个好的 JavaScript 调试工具,允许你查看你将要处理的网页的 DOM 树(参见第一章中的“FireBug 和 DevTool”一节)。
使用 D3 库的调试工具是很重要的,因为与你见过的其他库不同,它不是由预先建模的对象构成的。使用 D3,需要从头开始,逐个实现所有的图表元素。因此,熟悉开发的人会意识到,选择一个好的调试工具对于解决出现的任何问题都是至关重要的。
使用 FireBug 可以编辑、调试和监控 CSS、SVG 和 HTML。您可以实时更改它们的值并查看效果。它还提供了一个控制台,您可以在其中读取日志,日志被适当地放置在 JavaScript 代码中,以监控所使用的变量的内容。这可以通过调用控制台对象的log()
函数并将感兴趣的变量作为参数传递来实现:
console.log (variable);
也可以添加一些文本供参考:
console.log ("this is the value:");
您将看到,使用 D3 时,FireBug 对于检查 JavaScript 在 DOM 中生成的 SVG 元素的动态结构至关重要。
D3 简介
D3 是一个 JavaScript 库,类似于 jQuery 库,允许直接检查和操作 DOM,但仅用于数据可视化。它确实出色地完成了它的工作。实际上,D3 这个名字来源于数据驱动文档。D3 是由 Protovis 库的创建者 Mike Bostock 开发的,D3 的设计目的是取代它。
这个库被证明是非常通用和强大的,这要归功于它所基于的技术:JavaScript、SVG 和 CSS。D3 结合了强大的可视化组件和数据驱动的 DOM 操作方法。这样,D3 充分利用了现代浏览器的功能。
D3 允许您将任意数据绑定到 DOM。它的优势在于能够影响文档的几种转换。例如,一组数据可以转换成交互式 SVG 图形结构,比如图表。
您已经看到 jqPlot 作为一个 JavaScript 框架的优势在于它提供了结构化的解决方案,您可以通过选项的设置来操纵这些解决方案。与 jqPlot 不同,D3 的实力恰恰相反。它提供了构建块和工具来组装基于 SVG 的结构。这种方法的结果是新结构的不断发展,这是图形丰富,开放给各种各样的互动和动画。对于那些想要为现有框架没有涵盖的方面开发新的图形解决方案的人来说,D3 是一个完美的工具。
D3 不使用 jQuery 库,但是它有许多类似的概念,包括方法链范例和选择。它为 DOM 提供了一个类似 jQuery 的接口,这意味着您不需要非常详细地了解 SVG 的所有特性。为了处理 D3 代码,您需要能够使用对象和函数,并理解广泛使用的 SVG 和 CSS 的基础知识。为掌握所有这些知识所付出的牺牲会得到回报,你可以创造出令人惊叹的可视化效果。
SVG 为艺术作品提供了构建模块;它允许你画所有的基本形状,如直线、矩形、圆形和文本。它允许你用路径构建复杂的形状。
从一个空白的 HTML 页面开始
是时候实践刚刚概述的概念了。首先,从一个空白页开始,如清单 19-1 所示。这将是所有 D3 例子的起点。
清单 19-1。ch19_01a.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="
http://d3js.org/d3.v3.js
<style>
// CSS Style here
</style>
</head>
<body>
<!-- HTML elements here -->
<script type="text/javascript">
// D3 code here
</script>
</body>
</html>
虽然乍一看,您只看到一个简单的 HTML 空白页面,但是在使用 D3 时,您必须采取一些小措施。最简单明了的方法是包含库 D3:
<script src="../src/d3.v3.js"></script>
或者,如果您喜欢使用内容交付网络(CDN)服务:
<script src="
http://d3js.org/d3.v3.js
当输入远程 D3 库的 URL 时,确保网站总是包含最新版本。另一个不太明显的方法是添加页面的<head>
:
<meta charset="utf-8">
如果不指定这一行,您很快就会发现您添加的 D3 代码无法运行。最后,但同样重要的是,在哪里添加代码的各个部分非常重要。建议将 D3 的所有 JavaScript 代码放在<body>
部分的末尾,在所有 HTML 元素之后。
使用选择和运算符
要开始使用 D3,有必要熟悉选择的概念。处理选择涉及到三个基本对象的使用:
- 选择
- 选择器
- 经营者
选择是从当前文档中提取的节点元素的数组。为了提取一组特定的元素(选择),您需要使用选择器。这些模式匹配文档树结构中的元素。一旦你得到一个选择,你可能希望对它执行一些操作,所以你使用操作符。作为它们操作的结果,您得到一个新的选择,因此可以应用另一个操作符,以此类推。
选择器和操作符是由 W3C(万维网联盟)API 定义的,所有现代浏览器都支持。通常,您将操作 HTML 文档,因此您将选择 HTML 元素。
选择和选择器
为了从文档中提取选择,D3 提供了两种方法:
select
selectAll
选择与选择器匹配的第一个元素,返回只有一个元素的选择。
相反,选择所有与选择器匹配的元素,返回包含所有这些元素的选择。
要理解这些概念,最好的方法就是通过一些简单的例子来逐步理解。从刚才描述的 HTML 页面开始,添加包含一些文本的两段,然后用 D3 进行选择(见清单 19-2)。
清单 19-2。ch19_01a.html
<body>
<p>First paragraph</p>
<p>Second paragraph</p>
<script type="text/javascript">
var selection = d3.select("p");
console.log(selection);
</script>
</body>
d3.select
是顶级操作员;"p"
是选择器;而selection
是你赋给变量的运算符的返回值。使用这个 D3 命令,您想要选择 web 页面中的第一个元素<p>
。使用log
功能,你可以在图 19-1 中看到 FireBug 的选择。
图 19-1。
The FireBug console enables you to see the content of the selection
因为您使用了select()
方法,所以您的选择只有一个元素,尽管在 web 页面中有两个元素。如果你想两者都选,你可以使用selectAll()
,如清单 19-3 所示。
清单 19-3。ch19_01b.html
<script type="text/javascript">
var selection =
d3.selectAll("p");
console.log(selection);
</script>
图 19-2 显示了这两种元素。
图 19-2。
FireBug shows the selection of all the
elements in the web page
现在您有一个包含两个元素的选择。jQuery 和 D3 引入选择概念最大创新是不再需要循环。您可以一次对整个选择进行操作,而不是编写递归函数来修改元素。
经营者
一旦你学会了选择,是时候对它们应用操作符了。
操作符是一种应用于选择或一组元素的方法,它专门“操作”一个操作。例如,它可以获取或设置选择中元素的属性,或者以某种方式对其内容进行操作。例如,您可能想要用新文本替换现有文本。为此,您使用了text()
操作符,如清单 19-4 所示。
清单 19-4。ch19_02.html
<body>
<p>First paragraph</p>
<p>Second paragraph</p>
<script type="text/javascript">
var selection = d3.selectAll("p");
selection.text("we add this new text");
</script>
</body>
该页面现在为同一文本报告两次,而之前有两个段落(见图 19-3 )。
图 19-3。
The text contained in the two <p>
elements has been replaced in the browser on the left and is shown in FireBug on the right
您定义了变量选择,然后将运算符应用于该变量。但是还有另一种方式来写这一切;您可以使用链功能的方法,尤其是当您将多个运算符应用于同一个选择时。
d3.selectAll("p").text("we add this new text");
您已经看到,通过向text()
操作符传递一个参数,您将替换现有的文本。所以就好像函数是setText("new text")
。但是你并不总是想要那样。如果不传递任何参数,该函数将有不同的行为。它将返回已经存在的文本的值。这对于进一步的处理,或者将该字符串值赋给变量或数组非常有用。因此,如果没有参数,它就好像是getText()
。
var text = d3.select("p").text();
console.log(text);
文本变量包含"First paragraph"
字符串(见图 19-4 )。
图 19-4。
The FireBug console shows the text contained in the selection
你想要操作的每一种对象都有操作符。这些操作员可以设置以下内容:
- 属性
- 风格
- 性能
- 超文本标记语言
- 文本
您刚刚看到了text()
操作符的运行。接下来,您将看到其他一些操作符。
Note
如果你想了解更多关于操作符的知识,我建议你访问 D3 库的 API 参考,链接: https://github.com/mbostock/d3/wiki/API-Reference
。
例如,能够改变 CSS 样式是很有帮助的,你可以用style()
操作符来实现。清单 19-5 使用text()
替换了现有的文本,然后修改其样式为红色,在方法链中添加了style()
操作符。
清单 19-5。ch19_03.html
<body>
<p>Existing black text</p>
<script type="text/javascript">
d3.selectAll("p").style('color','red').text("New red text");
</script>
</body>
图 19-5 左侧显示原始文本,右侧显示新样式的文本。
图 19-5。
The original text is replaced by the new red text, applying the chain method upon the selection
另一个操作符attr()
,作用于元素的属性层。创建新的 SVG 元素时将使用此运算符;事实上,它允许您在创建标签时定义属性,然后再将它们插入到网页中。在这里,您可以看到它如何修改现有的属性。在清单 19-6 中,你正在改变显示在页面中间的标题的对齐方式(见图 19-6 )。
清单 19-6。ch19_04.html
<body>
<h1>Title</h1>
<script type="text/javascript">
d3.select('h1').attr('align','center');
</script>
</body>
图 19-6。
With the D3 library it is possible to dynamically add a title to a web page
创建新元素
既然您已经看到了如何在元素级别上操作以及如何修改属性和内容,那么是时候看看如何创建新的项目了。为此,D3 提供了许多运算符( https://github.com/mbostock/d3/wiki/API-Reference
,其中最常用的有:
html()
append()
insert()
html()方法
本节展示了html()
方法是如何操作的。您总是从一个选择开始,然后应用这个操作符在里面添加一个元素。例如,您选择一个特定的标签作为容器,然后编写一个字符串作为参数传递。然后这个字符串成为标签的内容(见清单 19-7)。
清单 19-7。ch19_05.html
<body>
<p>A paragraph</p>
<script type="text/javascript">
d3.select('p').html("<h1>New Paragraph</h1>");
</script>
</body>
这里,首先用select()
选择<p>
标签,然后用html()
用新元素<h1>
替换它的内容。图 19-7 左侧显示原始文本,右侧显示新格式化的版本。
图 19-7。
The text in a paragraph element <p>
is replaced with a heading element <h>
使用 FireBug 可以更好地看到这种变化(参见图 19-8
图 19-8。
FireBug clearly shows the insertion of the head element (on the right) to replace the content of the paragraph element (on the left)
实际上,html()
函数用作为参数传递的 HTML 代码替换选择的内容。顾名思义,这个函数允许您在选择的元素中动态编写 HTML 代码。
append()方法
另一种流行的添加元素的方法是append()
。
回想一下,当您使用html()
操作符时,所选标记的内容(如果有的话)将被替换为作为参数传递的新内容。append()
操作符改为添加一个新元素,作为它的参数传递到所选标签中包含的所有现有元素的末尾。新创建的元素的内容必须添加到方法链中,如果它只是一个字符串,则使用text()
,如果它是另一个元素,则使用append()
、html()
或insert()
。
为了理解这最后一点,在页面上添加一个无序列表<ul>
,其中包含一些包含水果名称的条目(见图 19-9 )。
图 19-9。
An unordered list of three fruits
假设您现在想将橙子添加到这个列表中。为此,您必须选择无序列表标签<ul>
,然后使用append()
添加一个列表项标签<li>
。但是append()
只创建了标签,所以为了将字符串"Oranges"
插入其中,你需要将text()
操作符添加到方法链中(见清单 19-8)。
清单 19-8。ch19_06.html
<body>
<ul>
<li>Apples</li>
<li>Pears</li>
<li>Bananas</li>
</ul>
<script type="text/javascript">
d3.select('ul').append('li').text("Oranges");
</script>
</body>
图 19-10 显示了添加了元素的列表。
图 19-10。
Using the append()
operator, you have added the Oranges item to the end of the list
图 19-11 在 FireBug 中显示。
图 19-11。
FireBug shows the HTML structure with the added <li>
element
在这种情况下,您已经使用简单的文本作为添加到列表中的新元素的内容,但是append()
操作符可以做更多的事情。事实上,如前所述,一个元素的内容可以是另一个元素。这允许你创建一个完整的 HTML 元素树,所有这些都是通过一个链式方法实现的。事实上,由append()
操作符创建的新元素的内容可以由另一个操作符创建,比如另一个append()
操作符。请看清单 19-9。这是一个简单的例子,可以帮助你更好地理解这个概念。
这一次,您想要创建一个水果的子类——柑橘类水果,在这个子类中,我们将分配橙子、柠檬和葡萄柚项目。为此,您需要添加一个新的列表项<li>
,以字符串"Citrus fruits"
作为其内容。这与上一个例子的工作方式相同,在append()
操作符之后连接text()
操作符。然后,您需要创建一个新的列表项。这一次,它的内容是一个无序列表。因此,您需要连接两个append()
操作符,以便创建一个嵌套在无序列表<ul>
元素中的列表项<li>
元素。然后,您可以向嵌套的无序列表中添加另外两个新元素,同样使用append()
操作符。
清单 19-9。ch19_06b.html
<body>
<ul>
<li>Apples</li>
<li>Pears</li>
<li>Bananas</li>
</ul>
<script type="text/javascript">
d3.select('ul').append('li').text("Citrus fruits");
d3.select('ul').append('ul').append('li').text("Oranges");
d3.select('ul').select('ul').append('li').text("Lemons");
d3.select('ul').select('ul').append('li').text("Grapefruits");
</script>
</body>
图 19-12 显示了浏览器上新的柑橘类水果嵌套列表以及在 FireBug 上生成它的 HTML 结构。
图 19-12。
FireBug shows the HTML structure with a nested unordered list in the browser on the left and in FireBug on the right
insert()方法
最后一个操作符insert()
有一个特殊的行为。如果只使用一个参数,它的行为就像使用append()
一样。通常,它与两个参数一起使用。第一个指示要添加的标记,第二个是要插入的匹配标记。实际上,将前面水果清单示例中的append()
替换为insert()
,会得到不同的结果,如图 19-13 (左边是原来的清单,右边是添加了橙子的新清单)。
d3.select('ul').insert('li','li').text("Oranges");
图 19-13。
Using the insert()
operator, you can insert the Oranges item at the top of the list
现在新元素位于无序列表的顶部。但是如果您想在不同于第一个项目的位置插入一个新项目呢?您可以使用 CSS 选择器nth-child(i)
来做到这一点,其中i
是元素的索引。因此,如果您使用选择器li:nth-child(i)
,您将选择第 I 个<li>
元素。因此,如果你想在第二个和第三个元素之间插入一个元素,你需要在insert()
操作符中调用第三个<li>
元素(记住这个操作符把新元素放在被调用的元素之前):
d3.select('ul').insert('li','li:nth-child(2)').text("Oranges");
这将在列表的第二个和第三个项目之间插入新的橙色项目,如图 19-14 所示(在左边的浏览器和右边的 FireBug 中)。
图 19-14。
Using the CSS selector nth-child, you can add the Oranges item in any position in the list HTML(), APPEND(), AND INSERT() OPERATORS: A BETTER UNDERSTANDING
有时,理解这三个操作符的功能并不容易。考虑这个示意性的 HTML 结构,其中包含一个通用的父标签和一些子标签:
<parent>
<child></child>
<child></child>
<child></child>
</parent>
为了更好地理解不同的行为,下面的简单图表显示了每个操作符具体做什么。如果您想充分利用 D3 库的潜力,完全理解这三个操作符的功能是至关重要的。
当您需要在 HTML 结构的同一层的其他标签列表的末尾创建一个新的标签元素时,使用append()
操作符。图 19-15 显示了该操作员的行为。
图 19-15。
The append()
operator adds a child tag to the end of the list
当您需要在 HTML 结构的同一层的其他标签列表的开头创建一个新的标签元素时,使用insert()
操作符。图 19-16 显示了该操作员的行为。
图 19-16。
The insert()
operator adds a child tag before the child tag is passed as a second argument
当您需要在其他标签列表中的特定位置创建一个新的标签元素时,总是在 HTML 结构的同一层,使用insert()
操作符。图 19-17 显示了该操作员的行为。
图 19-17。
You can pass a child of the list as the argument using the CSS selector nth-child()
当你需要创建一个新的标签元素来代替另一个标签或者 HTML 结构中同一层的一系列标签时,使用html()
操作符。图 19-18 说明了该操作员的行为。
图 19-18。
The html()
operator replaces the contents of the parent tag with the tag passed as the argument
将数据插入元素
您已经看到了如何在文档中创建新的元素。但是你怎么能把数据放进去呢?这就是data()
操作符的用武之地,它将一组数据作为参数传递。
对于选择中的每个元素,将按照序列的相同顺序在数组中赋值。这种对应关系由一个通用函数表示,其中d
和i
作为参数传递。
function(d,i) {
// code with d and i
// return some elaboration of d;
}
只要列表中有元素,这个函数就会执行很多次:i
是序列的索引,d
是对应于该索引的数据数组中的值。很多时候你对i
的值不感兴趣,只用d
。
对于那些熟悉for
循环的人来说,就好像你写了:
for(i=0; i < selection.length; i++){
d = input_array[i];
// code with d and i
//return output_array[i];
}
要了解整个事情,没有比提供一个例子更好的方法了。定义一个包含三种水果名称的数组。您将创建一个包含三个空条目的无序列表,并用selectAll()
创建这些条目的选择。选择中必须有相应数量的项目,数组中必须有相应数量的值;否则,将不评估剩余价值。您将数组与选择相关联,然后应用function(d)
,在列表中写入每一项的值(参见清单 19-10)。
清单 19-10。ch19_07.html
<body>
<ul>
<li></li>
<li></li>
<li></li>
</ul>
<script type="text/javascript">
var fruits = ['Apples', 'Pears', 'Bananas'];
d3.selectAll('li').data(fruits).text( function(d){
return d;
});
</script>
</body>
图 19-19 显示了左侧浏览器和右侧 FireBug 中的结果。在 FireBug 中,您可以看到用于每个列表项<li>
内容的 HTML 结构,这在您编写清单 19-10 时是不存在的。这些添加的文本项是fruits
数组的值。
图 19-19。
It is possible to fill the content of HTML elements with array values
data()
操作符不仅仅将数据绑定到元素,它还计算选择的元素和提供的数据之间的联系。只要数组的长度等于所选元素的数量,一切都会很顺利。但如果不是这样呢?如果您有一个包含三个元素的选择,并提供一个包含五个值的数组,那么这两个额外的值将存储在一个名为“enter”的特殊选择中这个选项可以通过数据调用返回值上的enter()
操作符来访问。你可以在清单 19-11 的例子中看到这一点。
清单 19-11。ch19_08.html
<body>
<ul>
<li></li>
<li></li>
<li></li>
</ul>
<script type="text/javascript">
var fruits = ['Apples', 'Pears', 'Bananas', 'Oranges', 'Strawberries'];
var list = d3.select('ul');
var fruits = list.selectAll('li').data(fruits);
fruits.enter().append('li').text( function(d){
return d;
});
fruits.text( function(d){
return d;
});
</script>
</body>
首先,用五种不同的水果定义数组。然后,选择包含列表的内容,并将其分配给变量列表。从这个选择中,您可以进一步选择包含三个空列表项的列表项,并将fruits
数组分配给它。从这个关联中,数组的最后两个值将前进(橙子和草莓),因此它们将被存储在enter
选择中。现在你必须特别注意这一点:通常最好先处理enter
选择。因此,您必须访问enter
选项并使用append()
来创建两个新的列表项,其中包含两个高级水果。然后,在三个现有列表项的水果选择中写入值。
您将得到一个包含所有五种水果的列表,按照输入的顺序排列。图 19-20 顶部显示了浏览器的变化,底部显示了 FireBug 的变化。
图 19-20。
It is possible to fill the content of HTML elements with array values and to integrate them with other elements if they are not enough
应用动态属性
您已经看到了如何使用 D3 框架提供的函数来定义和修改样式、属性和其他属性。但到目前为止,它们都被当作常数。是时候向前迈出一大步了。JavaScript 语言的优势之一,尤其是 D3(和 jQuery)库,在于它能够使页面内容动态化。事实上,您已经看到了如何在 web 页面中删除、创建和操作元素标签。类似的方法也适用于其他类型的值,如 CSS 样式或通过选择机制创建或操作的元素的属性。您甚至可以创建与事件或控件相关的不同选项。
D3 为此提供了一组特定的函数。尽管这些函数看起来很简单,但对于那些知道如何充分利用其机制的人来说,它们可能是一个强大的工具。
在清单 19-12 的例子中,你使用一个普通的函数给段落分配一个随机的颜色。每次加载页面时,它都会显示一组不同的颜色。
清单 19-12。ch19_09.html
<body>
<p>the first paragraph</p>
<p>the second paragraph</p>
<p>the third paragraph</p>
<p>the last paragraph</p>
<script>
d3.selectAll("p").style("color", function() {
r = Math.round((Math.random() * 255));
g = Math.round((Math.random() * 255));
b = Math.round((Math.random() * 255));
return "rgb("+r+", "+g+", "+b+")";
});
</script>
</body>
左边的图 19-21 显示了一个加载页面和另一个页面的结果,右边的页面应用了不同的颜色。每次加载页面,都会得到不同的颜色组合。
图 19-21。
The colors change each time the page loads
当然,这是一个非常简单的例子,但是它展示了基本的思想。分配给属性、文本或样式的任何值都可以通过函数动态生成。
添加 SVG 元素
你终于可以运用你所学的知识来创造漂亮的展示了。在本节中,您将开始学习 D3 库的特性,创建和操作图形元素,如直线、正方形、圆形等等。所有这些将主要通过使用两个标签的嵌套结构来完成:<svg>
用于图形元素,<g>
用于应用组。
首先,您将学习如何创建一个 SVG 元素,以及如何使用<g>
标签将它嵌套在一个组中。稍后,您将发现什么是 SVG 转换,以及如何将它们应用于元素组。最后,通过另一个例子,您将看到如何用 SVG 转换来制作这些元素的动画,以获得漂亮的动画。
创建 SVG 元素
您可以从一个<div>
标签开始,它将被用作可视化的容器,类似于 jQuery 对<canvas>
所做的。从这个<div>
标签,您使用append()
操作符创建根标签<svg>
。然后你可以通过使用attr()
操作符作用于height
和width
属性来设置可视化的大小(见清单 19-13)。
清单 19-13。ch19_10.html
<body>
<div id="circle"></div>
<script type="text/javascript">
var svg = d3.select('#circle')
.append('svg')
.attr('width', 200)
.attr('height', 200);
</script>
</body>
从 FireBug 中,你可以看到带有新的<svg>
元素的<body>
结构及其属性(见图 19-22 )。
图 19-22。
FireBug shows the <svg>
tag you just created
您还可以向根标签<svg>
添加一个基本形状。让我们添加一个黄色的圆圈(见清单 19-14)。一旦你理解了这个原则,无论何时你想重复它都是非常简单的。
清单 19-14。ch19_10.html
<script type="text/javascript">
var svg = d3.select('#circle')
.append('svg')
.attr('width', 200)
.attr('height', 200);
svg.append('circle')
.style('stroke', 'black')
.style('fill', 'yellow')
.attr('r', 40)
.attr('cx', 50)
.attr('cy', 50);
</script>
图 19-23 显示了完美的黄色圆圈。
图 19-23。
A perfect yellow circle
在 FireBug 中,你可以看到标签的树形结构是如何从根<svg>
开始逐渐形成的,指定了所有的属性(见图 19-24 )。
图 19-24。
In FireBug, it is possible to follow the development of the tag structure
现在您已经看到了如何使用 SVG 标签创建图形,下一步是对它们应用转换。
转换
D3 的一个关键方面是它的转换能力。这扩展了 JavaScript 中 SVG 转换的概念。一旦在 SVG 中创建了一个对象,从简单的正方形到更复杂的结构,它都可以进行各种变换。最常见的转换包括:
- 规模
- 翻译
- 辐状的
Note
如果你有兴趣了解更多关于转换的知识,我建议你访问这个页面: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
。它列出了所有可用的转换,并附有简单的解释。
通常,您使用这些基本转换的序列来获得更复杂的转换。和往常一样,您将看到一系列小例子来说明转换的概念。首先,你将画一个红色的小方块,就像你画黄色圆圈一样(见清单 19-15)。为此,您可以使用<rect>
标签。与<circle>
唯一不同的是,对于矩形,你需要用 x 和 y 来指定矩形左上角的位置,而不是圆心。然后你要指定矩形的大小,既然是正方形,那么边就要相等。
清单 19-15。ch19_11a.html
<div id="square"></div>
<script type="text/javascript">
var svg = d3.select(``'#square'
.append('svg')
.attr('width', 200)
.attr('height', 200);
svg.append('rect')
.style('stroke', 'black')
.style('fill', 'red')
.attr('x', 50)
.attr('y', 50)
.attr('width', 50)
.attr('height', 50);
</script>
现在是引入另一个概念的好时机,这个概念在处理 SVG 元素时会很有用:元素组。您通常需要对一组形状或一个复杂形状(由多个基本形状组成)应用一系列操作,有时仅包括变换。这可以通过将几个项目组合在一个组中来实现,这可以通过将所有元素放在一个标签<g>
中在 SVG 中反映出来。例如,如果你想对红色方块应用一个变换,你需要把它插入一个组中(见清单 19-16)。
清单 19-16。ch19_11a.html
var svg = d3.select('#square')
.append('svg')
.attr('width', 200)
.attr('height', 200);
var g = svg.append("svg:g");
g.append('rect')
.style('stroke', 'black')
.style('fill', 'red')
.attr('x', 50)
.attr('y', 50)
.attr('width', 50)
.attr('height', 50);
图 19-25 显示了 SVG 结构在 FireBug 中的表现。
图 19-25。
FireBug shows the SVG structure corresponding to the red square
在浏览器中,你会看到一个如图 19-26 所示的红色小方块。
图 19-26。
A red square is a good object upon which to apply transformations
现在,您将一个接一个地应用所有的变换。从平移开始,在 SVG 中由translate
(x, y)
函数表示,其中x
和y
是方块移动的像素数量(见清单 19-17)。
清单 19-17。ch19_11b.html
var g = svg.append("svg:g")
.attr("transform", "translate(" + 100 + ",0)");
在这里,我将值100
放在作为属性传递的字符串之外,以便理解在这一点上您可以插入一个先前定义的变量。这将使转变更有活力。用这条线,你将正方形向右移动了 100 个像素(见图 19-27 )。
图 19-27。
Now the red square appears right-shifted by 100 pixels
另一种可以应用于正方形的变换叫做缩放。在这个 SVG 中,通过函数scale(s)
或scale(sx, sy)
来表示。如果在函数中传递单个参数,缩放将是一致的,但如果传递两个参数,您可以在水平和垂直方向上以不同的方式应用正方形的扩展。清单 19-18 将红色方块的大小增加了两倍。因此,您需要应用scale()
转换,并将值2
作为参数传递。传递的数字是正方形大小要乘以的因子。因为您已经传递了一个参数,所以缩放是一致的。
清单 19-18。ch19_11c.html
var g = svg.append("svg:g")
.attr("transform","scale(2)");
图 19-28 显示了缩放了两倍的正方形。这个广场的高度和宽度都增加了一倍。
图 19-28。
The red square has doubled its size
如果你想要非均匀缩放,你可以使用类似清单 19-19 的东西来获得类似图 19-29 的结果。不均匀的缩放会扭曲一个图形,从而产生另一个图形。在这种情况下,你从一个正方形得到一个矩形。
清单 19-19。ch19_11d.html
var g = svg.append("svg:g")
.attr("transform","scale(2, 1)");
图 19-29。
A rectangle obtained by applying non-uniform scaling to a square
最后一种变换是旋转。用函数rotate(degree,x,y)
在 SVG 中表示,其中第一个自变量是以度为单位的旋转(顺时针)角度,x
和y
是旋转中心的坐标。
假设您希望旋转的中心与正方形的中心相对应,正方形位于 x = 75 和 y = 75 处。如果你想画一个菱形,你需要在正方形上旋转 45 度(见清单 19-20)。
清单 19-20。ch19_11e.html
var g = svg.append("svg:g")
.attr("transform","rotate(45, 75, 75)");
你得到菱形(见图 19-30 )。
图 19-30。
A rhombus is the result you obtain when you rotate a square
但是最有趣的效果涉及到在一个序列中应用转换,从而创建一个链(见清单 19-21)。
清单 19-21。ch19_11f.html
var g = svg.append("svg:g")
.
attr("transform", "translate(-30, 0),scale(2, 1),rotate(45, 75, 75)");
从这个列表中,你得到了图 19-31 中的形状。
图 19-31。
A rhombus obtained by applying a chain of transformations to a square
过渡
您已经看到,属性、样式等的值可以是动态的,这取决于借助某些函数设置的定义。但是 D3 提供了更多——你甚至可以动画你的形状。D3 为此提供了三个功能:
transition()
delay()
duration()
很自然,您会将这些函数应用于 SVG 元素,这要感谢 D3,它可以识别任何类型的值并对它们进行插值。
当 SVG 形状从一种状态转换到另一种状态时,您可以定义转换。起始状态和最终状态都由几个参数来表征,这些参数定义了对象的颜色、形状、大小和位置。你把黄圈例子中定义的状态作为初始状态(参见清单 19-14)。在清单 19-22 中,你让圆经历一个由三种不同变化组成的过渡:圆把它的颜色变成黑色(把fill
设置成black
),它缩小它的面积(把r
从40
变成10
),它稍微向右移动(把cx
从50
变成150
)。
清单 19-22。ch19_12.html
<div id="circle"></div>
<script>
var svg = d3.select('#circle')
.append('svg')
.attr('width', 200)
.attr('height', 200);
svg.append('circle')
.style('stroke', 'black')
.style('fill', 'yellow')
.attr('r', 40)
.attr('cx', 50)
.attr('cy', 50)
.transition()
.delay(100)
.duration(4000)
.attr("r", 10)
.attr("cx", 150)
.style("fill", "black");
</script>
因此,在这个例子中,您将transition()
方法添加到方法链中。这将初始状态与最终状态分开,并警告 D3 发生了转换。紧随transition()
之后的是另外两个功能:delay()
和duration()
。
delay()
函数有一个参数:转换开始前必须经过的时间。相反,duration()
函数被定义为过渡所用的时间。传递的参数值越大,过渡越慢。
在这三个函数之后,您将所有表征图形最终状态的属性添加到方法链中。D3 根据你建立的时间来插入中间值,并且会用这些值生成所有的中间数字。出现在你眼前的是一个动画,黄色的圆圈变成黑色,向左移动,尺寸减小。所有这一切只需要四秒钟。
图 19-32 显示了过渡序列,由此可以看到圆的变化。
图 19-32。
Different instances of the animation of a circle subjected to transitions
到目前为止,您看到的简单示例一次应用于一个图形元素。下一步是将你所学到的应用到元素组中,从而创建更复杂的图形。后续章节提供了将 D3 库的基本概念付诸实践的好例子。
摘要
本章介绍了 D3 库的亮点。即使不使用 jQuery 库,D3 也能以非常相似的方式管理选择、选择器和操作符。通过一系列示例,您已经看到了如何通过更改 DOM 元素的属性以及在需要时创建新的属性来操作 DOM 元素。在本章的第二部分,你学习了 D3 库操作的主要对象是什么:SVG 元素。这些是构建图表的图形构件。最后,您快速查看了如何将 SVG 转换应用于这些图形元素,然后查看了如何利用 SVG 转换来生成漂亮的动画。
下一章通过实现折线图,将你到目前为止学到的关于 D3 库的知识付诸实践。通过一个接一个的 SVG 元素,您将看到如何获得与 jqPlot 和 Highcharts 库类似的结果。
二十、D3 折线图
Abstract
在本章中,您将创建一个带有刻度和标签的折线图。D3 不像 jqPlot 那样是一个图表框架。但是,它允许您向文档中添加可缩放矢量图形(SVG)元素,通过操作这些元素,您可以创建任何类型的可视化。这种灵活性使您能够构建任何类型的图表,一砖一瓦地构建。
在本章中,您将创建一个带有刻度和标签的折线图。D3 不像 jqPlot 那样是一个图表框架。但是,它允许您向文档中添加可缩放矢量图形(SVG)元素,通过操作这些元素,您可以创建任何类型的可视化。这种灵活性使您能够构建任何类型的图表,一砖一瓦地构建。
首先,您将了解如何使用上一章介绍的 D3 命令构建折线图的基本元素。特别是,您将分析经常遇到的比例、域和范围的概念。就如何管理值集而言,这些构成了 D3 库的一个典型方面。
一旦您理解了如何管理值域、刻度和区间中的值,您就可以开始实现图表组件了,比如轴、轴标签、标题和网格。这些组件构成了绘制折线图的基础。与 jqPlot 不同,这些组件并不容易获得,而是必须逐步开发。这将导致额外的工作,但也将使您能够创建特殊的功能。你的 D3 图表将能够响应特殊的需求,或者至少,他们将有一个完全原始的外观。举例来说,您将看到如何向轴添加箭头。
D3 库的另一个特点是使用读取文件中包含的数据的函数。您将看到这些函数是如何工作的,以及如何利用它们来满足您的需求。
一旦掌握了实现折线图的基本知识,您将看到如何实现多系列折线图。您还将了解如何实现图例以及如何将其与图表相关联。
最后,作为总结,您将分析折线图的一种特殊情况:差异折线图。这将有助于您理解剪辑区域路径——它们是什么以及它们的用途是什么。
用 D3 开发折线图
您将开始使用 D3 库最终实现您的图表。在本节和接下来的几节中,您将发现一种不同于 jqPlot 和 Highcharts 等库所采用的图表实现方法。这里的实现是在一个较低的层次,代码更长更复杂;然而,没有什么是你够不着的。
现在,一步一步,或者更好,一砖一瓦,你会发现如何产生一个折线图和组成它的元素。
从第一块砖开始
开始的第一块“砖”是在您的 web 页面中包含 D3 库(有关更多信息,请参见附录 A):
<script src="../src/d3.v3.min.js"></script>
或者,如果您喜欢使用内容交付网络(CDN)服务:
<script src="
http://d3js.org/d3.v3.min.js
下一个“砖块”由清单 20-1 中的输入数据数组组成。该数组包含数据系列的 y 值。
清单 20-1。ch20_01.html
var data = [100, 110, 140, 130, 80, 75, 120, 130, 100];
清单 20-2 定义了一组与绘制图表的可视化尺寸相关的变量。w
和h
变量是图表的宽度和高度;页边距用于在图表边缘留出空间。
清单 20-2。ch20_01.html
w = 400;
h = 300;
margin_x = 32;
margin_y = 20;
因为您在基于 x 轴和 y 轴的图形上工作,所以在 D3 中有必要为这两个轴中的每一个定义一个标度、一个域和一个值范围。我们先来理清这些概念,了解一下它们在 D3 中是如何管理的。
比例、域和范围
你已经不得不处理规模,即使你可能没有意识到这一点。线性标度更容易理解,尽管在一些例子中,你已经使用了对数标度(参见第九章中的侧栏“对数标度”)。标度只是将某个区间(称为域)中的值转换为属于另一个区间(称为范围)的另一个值的函数。但是这一切到底意味着什么呢?这对你有什么帮助?
实际上,每当您想要影响属于不同区间的两个变量之间的值的转换,但保持其相对于当前区间的“意义”时,这都可以为您服务。这涉及到规范化的概念。
假设您想要转换来自仪器的值,例如万用表报告的电压。您知道电压值应该在 0 到 5 伏之间,这是值的范围,也称为域。
您想要转换在红色标尺上测得的电压。使用红绿蓝(RGB)代码,该值将介于 0 和 255 之间。您现在已经定义了另一个颜色范围,即范围。
现在假设万用表上的电压读数是 2.7 伏,红色显示的色标对应的是 138(实际是 137.7)。您刚刚对值的转换应用了线性标度。图 20-1 显示了电压值到 RGB 刻度上相应 R 值的转换。这种转换在线性范围内进行,因为值是线性转换的。
图 20-1。
The conversion from the voltage to the R value is managed by the D3 library
但是这一切有什么用呢?首先,当您希望在图表中可视化数据时,不同间隔之间的转换并不罕见,其次,这种转换完全由 D3 库管理。你不需要做任何计算;您只需要定义要应用的域、范围和规模。
把这个例子翻译成 D3 代码,你可以写:
var scale = d3.scale.linear(),
.domain([0,5]),
.range([0,255]);
console.log(Math.round(scale(2.7))); //it returns 138 on FireBug console
代码内部
可以定义规模、定义域、范围;因此,您可以通过在代码中添加清单 20-3 来继续实现折线图。
清单 20-3。ch20_01.html
y = d3.scale.linear().domain([0, d3.max(data)]).range([0 + margin_y, h - margin_y]);
x = d3.scale.linear().domain([0, data.length]).range([0 + margin_x, w - margin_x]);
因为输入数据数组是一维的,并且包含要在 y 轴上表示的值,所以可以将域从 0 扩展到数组的最大值。你不需要使用一个for
循环来寻找这个值。D3 提供了一个名为max(date)
的特定函数,其中传递的参数是要在其中找到最大值的数组。
现在是开始添加 SVG 元素的时候了。要添加的第一个元素是<svg>
元素,它表示您要添加的所有其他元素的根。<svg>
标签的功能有点类似于 jQuery 和 jqPlot 中画布的功能。因此,你需要用w
和h
来指定画布的大小。在<svg>
元素内部,您添加了一个<g>
元素,这样所有内部添加到它的元素将被组合在一起。
随后,对这组<g>
元素进行转换。在这种情况下,转换包括坐标网格的平移,向下移动h
个像素,如清单 20-4 所示。
清单 20-4。ch20_01.html
var svg = d3.select("body")
.append("svg:svg")
.attr("width", w)
.attr("height", h);
var g = svg.append("svg:g")
.attr("transform", "translate(0," + h + ")");
创建折线图的另一个基本要素是路径元素。该路径由使用d
属性的数据填充。
手动输入所有这些值太麻烦了,在这方面,D3 提供了一个函数来帮您完成这个任务:d3.svg.line
。因此,在清单 20-5 中,你声明了一个名为line
的变量,其中的所有数据都被转换成一个点(x,y)。
清单 20-5。ch20_01.html
var line = d3.svg.line()
.x(function(d,i) { return x(i); })
.y(function(d) { return -1 * y(d); });
正如你将看到的,在所有需要扫描数组的情况下(一个for
循环),在 D3 里,这样的扫描通过使用参数d
和i
被不同地处理。数组当前项的索引用i
表示,而当前项用d
表示。回想一下,您通过变换将 y 轴向下平移。你需要保持这种心态;如果要正确画线,必须使用 y 的负值,这就是为什么要将d
值乘以-1。
下一步是给一个path
元素分配一行(见清单 20-6)。
清单 20-6。ch20_01.html
g.append("svg:path").attr("d", line(data));
如果您在这里停下来并启动页面上的 web 浏览器,您将得到如图 20-2 所示的图像。
图 20-2。
The default behavior of an SVG path element is to draw filled areas
这似乎是错误的,但是您必须考虑到在使用 SVG 创建图像时,由 CSS 样式管理的角色是占优势的。事实上,你可以简单地添加清单 20-7 中的 CSS 类来获得数据行。
清单 20-7。ch20_01.html
<style>
path {
stroke: steelblue;
stroke-width: 3;
fill: none;
}
line {
stroke: black;
}
</style>
因此,适当定义 CSS 样式类后,你会得到如图 20-3 所示的一行。
图 20-3。
The SVG path element draws a line if the CSS style classes are suitably defined
但是你还远没有一个折线图。您必须将两个轴相加。要绘制这两个对象,可以使用简单的 SVG 线条,如清单 20-8 所示。
清单 20-8。ch20_01.html
// draw the x axis
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -y(0))
.attr("x2", x(w))
.attr("y2", -y(0))
// draw the y axis
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -y(0))
.attr("x2", x(0))
.attr("y2", -y(d3.max(data))-10)
现在是添加标签的时候了。为此,有一个大大简化工作的 D3 函数:ticks()
。此函数应用于 D3 刻度,如 x 或 y,并返回四舍五入后的数字作为刻度。您需要使用函数text(String)
来获得当前d
的字符串值(参见清单 20-9)。
清单 20-9。ch20_01.html
//draw the xLabels
g.selectAll(".xLabel")
.data(x.ticks(5))
.enter().append("svg:text")
.attr("class", "xLabel")
.text(String)
.attr("x", function(d) { return x(d) })
.attr("y", 0)
.attr("text-anchor", "middle");
// draw the yLabels
g.selectAll(".yLabel")
.data(y.ticks(5))
.enter().append("svg:text")
.attr("class", "yLabel")
.text(String)
.attr("x", 25)
.attr("y", function(d) { return -y(d) })
.attr("text-anchor", "end");
为了对齐标签,您需要指定属性text-anchor
。它的可能值是middle
、start
和end
,这取决于您希望标签分别居中对齐、向左对齐还是向右对齐。
这里,您使用 D3 函数attr()
来指定属性,但是也可以在 CSS 样式中指定它,如清单 20-10 所示。
清单 20-10。ch20_01.html
.xLabel {
text-anchor: middle;
}
.yLabel {
text-anchor: end;
}
事实上,写这几行几乎是一回事。然而,通常,当您计划更改这些值时,您会更喜欢在 CSS 样式中设置它们——它们被理解为参数。相反,在这种情况下,或者如果您希望它们是一个对象的固定属性,最好使用attr()
函数来插入它们。
现在,您可以将记号添加到轴上。这是通过为每个刻度画一条短线来获得的。您对刻度标签所做的事情现在也同样适用于刻度,如清单 20-11 所示。
清单 20-11。ch20_01.html
//draw the x ticks
g.selectAll(".xTicks")
.data(x.ticks(5))
.enter().append("svg:line")
.attr("class", "xTicks")
.attr("x1", function(d) { return x(d); })
.attr("y1", -y(0))
.attr("x2", function(d) { return x(d); })
.attr("y2", -y(0)-5)
// draw the y ticks
g.selectAll(".yTicks")
.data(y.ticks(5))
.enter().append("svg:line")
.attr("class", "yTicks")
.attr("y1", function(d) { return -y(d); })
.attr("x1", x(0)+5)
.attr("y2", function(d) { return -y(d); })
.attr("x2", x(0))
图 20-4 为该阶段的折线图。
图 20-4。
Adding the two axes and the labels on them, you finally get a simple line chart
如您所见,您已经有了一个折线图。也许通过添加一个网格,如清单 20-12 所示,你可以让事情看起来更好。
清单 20-12。ch20_01.html
//draw the x grid
g.selectAll(".xGrids")
.data(x.ticks(5))
.enter().append("svg:line")
.attr("class", "xGrids")
.attr("x1", function(d) { return x(d); })
.attr("y1", -y(0))
.attr("x2", function(d) { return x(d); })
.attr("y2", -y(d3.max(data))-10);
// draw the y grid
g.selectAll(".yGrids")
.data(y.ticks(5))
.enter().append("svg:line")
.attr("class", "yGrids")
.attr("y1", function(d) { return -y(d); })
.attr("x1", x(w))
.attr("y2", function(d) { return -y(d); })
.attr("x2", x(0));
你可以对 CSS 样式做一些小的添加(见清单 20-13 ),以得到一个浅灰色的网格作为折线图的背景。此外,您可以定义更合适的文本样式,例如选择 Verdana 作为字体,大小为 9。
清单 20-13。ch20_01.html
<style>
path {
stroke: steelblue;
stroke-width: 3;
fill: none;
}
line {
stroke: black;
}
.xGrids {
stroke: lightgray;
}
.yGrids {
stroke: lightgray;
}
text {
font-family: Verdana;
font-size: 9pt;
}
</style>
现在用浅灰色网格绘制折线图,如图 20-5 所示。
图 20-5。
A line chart with a grid covering the blue lines
仔细看图 20-5 。网格的灰色线绘制在代表数据的蓝色线上方。换句话说,更明确地说,您必须注意绘制 SVG 元素的顺序。事实上,首先绘制轴和网格,然后最终移动到输入数据的表示上是很方便的。因此,你需要把你想画的所有项目按正确的顺序排列,如清单 20-14 所示。
清单 20-14。ch20_01.html
<script>
var data = [100,110,140,130,80,75,120,130,100];
w = 400;
h = 300;
margin_x = 32;
margin_y = 20;
y = d3.scale.linear().domain([0, d3.max(data)]).range([0 + margin_y, h - margin_y]);
x = d3.scale.linear().domain([0, data.length]).range([0 + margin_x, w - margin_x]);
var svg = d3.select("body")
.append("svg:svg")
.attr("width", w)
.attr("height", h);
var g = svg.append("svg:g")
.attr("transform", "translate(0," + h + ")");
var line = d3.svg.line()
.x(function(d,i) { return x(i); })
.y(function(d) { return -y(d); });
// draw the y axis
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -y(0))
.attr("x2", x(w))
.attr("y2", -y(0));
// draw the x axis
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -y(0))
.attr("x2", x(0))
.attr("y2", -y(d3.max(data))-10);
//draw the xLabels
g.selectAll(".xLabel")
.data(x.ticks(5))
.enter().append("svg:text")
.attr("class", "xLabel")
.text(String)
.attr("x", function(d) { return x(d) })
.attr("y", 0)
.attr("text-anchor", "middle");
// draw the yLabels
g.selectAll(".yLabel")
.data(y.ticks(5))
.enter().append("svg:text")
.attr("class", "yLabel")
.text(String)
.attr("x", 25)
.attr("y", function(d) { return -y(d) })
.attr("text-anchor", "end");
//draw the x ticks
g.selectAll(".xTicks")
.data(x.ticks(5))
.enter().append("svg:line")
.attr("class", "xTicks")
.attr("x1", function(d) { return x(d); })
.attr("y1", -y(0))
.attr("x2", function(d) { return x(d); })
.attr("y2", -y(0)-5);
// draw the y ticks
g.selectAll(".yTicks")
.data(y.ticks(5))
.enter().append("svg:line")
.attr("class", "yTicks")
.attr("y1", function(d) { return -1 * y(d); })
.attr("x1", x(0)+5)
.attr("y2", function(d) { return -1 * y(d); })
.attr("x2", x(0));
//draw the x grid
g.selectAll(".xGrids")
.data(x.ticks(5))
.enter().append("svg:line")
.attr("class", "xGrids")
.attr("x1", function(d) { return x(d); })
.attr("y1", -y(0))
.attr("x2", function(d) { return x(d); })
.attr("y2", -y(d3.max(data))-10);
// draw the y grid
g.selectAll(".yGrids")
.data(y.ticks(5))
.enter().append("svg:line")
.attr("class", "yGrids")
.attr("y1", function(d) { return -1 * y(d); })
.attr("x1", x(w))
.attr("y2", function(d) { return -y(d); })
.attr("x2", x(0));
// draw the x axis
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -y(0))
.attr("x2", x(w))
.attr("y2", -y(0));
// draw the y axis
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -y(0))
.attr("x2", x(0))
.attr("y2", -y(d3.max(data))+10);
// draw the line of data points
g.append("svg:path").attr("d", line(data));
</script>
图 20-6 显示了以正确顺序绘制的元素的折线图。事实上,代表输入数据的蓝线现在位于覆盖网格的前景上,而不是相反。
图 20-6。
A line chart with a grid drawn correctly
使用具有(x,y)值的数据
到目前为止,您已经使用了一个仅包含 y 值的输入数据数组。通常,您会想要表示分配了 x 和 y 值的点。因此,您将使用清单 20-15 中的输入数据数组来扩展前一种情况。
清单 20-15。ch20_02.html
var data = [{x: 0, y: 100}, {x: 10, y: 110}, {x: 20, y: 140},
{x: 30, y: 130}, {x: 40, y: 80}, {x: 50, y: 75},
{x: 60, y: 120}, {x: 70, y: 130}, {x: 80, y: 100}];
现在您可以看到数据是如何用包含 x 和 y 值的点来表示的。当您使用一个数据序列时,您通常需要立即确定 x 和 y 的最大值(有时还有最小值)。在前一个例子中,您使用了d3.max
和d3.min
函数,但是这些函数只对数组起作用,对对象不起作用。您插入的输入数据数组是一个对象数组。这个怎么解决?有几种方法。也许最直接的方法是扫描数据,找出 x 和 y 的最大值。在清单 20-16 中,你定义了两个包含这两个最大值的变量。然后一次扫描每个物体的 x 和 y 的值,你将 x 和 y 的当前值与xMax
和yMax
的值进行比较,看哪个值更大。两者中较大的将成为新的最大值。
清单 20-16。ch20_02.html
var xMax = 0, yMax = 0;
data.forEach(function(d) {
if(d.x > xMax)
xMax = d.x;
if(d.y > yMax)
yMax = d.y;
});
有几个有用的 D3 函数可以处理数组,那么为什么不直接从对象的输入数组创建两个数组呢——一个包含 x 的值,另一个包含 y 的值。你可以在任何需要的时候使用这两个数组,而不是使用对象数组,后者要复杂得多(见清单 20-17)。
清单 20-17。ch20_02.html
var ax = [];
var ay = [];
data.forEach(function(d,i){
ax[i] = d.x;
ay[i] = d.y;
})
var xMax = d3.max(ax);
var yMax = d3.max(ay);
这一次你把 x 和 y 都分配给数据点行,如清单 20-18 所示。即使在处理一组对象时,这个操作也非常简单。
清单 20-18。ch20_02.html
var line = d3.svg.line()
.x(function(d) { return x(d.x); })
.y(function(d) { return -y(d.y); })
至于代码的其余部分,没有太多要修改的——只有对 x 和 y 边界值的一些修正,如清单 20-19 所示。
清单 20-19。ch20_02.html
y = d3.scale.linear().domain(0,``yMax
x = d3.scale.linear().domain([0,``xMax
...
// draw the y axis
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -y(0))
.attr("x2", x(0))
.attr("y2", -y(yMax)-20)
...
//draw the x grid
g.selectAll(".xGrids")
.data(x.ticks(5))
.enter().append("svg:line")
.attr("class", "xGrids")
.attr("x1", function(d) { return x(d); })
.attr("y1", -y(0))
.attr("x2", function(d) { return x(d); })
.attr("y2", -y(yMax)-10)
// draw the y grid
g.selectAll(".yGrids")
.data(y.ticks(5))
.enter().append("svg:line")
.attr("class", "yGrids")
.attr("y1", function(d) { return -1 * y(d); })
.attr("x1", x(xMax)+20)
.attr("y2", function(d) { return -1 * y(d); })
.attr("x2", x(0))
图 [20-7 显示了为处理输入数据数组引入的 y 值所做的更改的结果。
图 20-7。
A line chart with a grid and axis labels that take into account the y values entered with the input array
控制轴的范围
在您刚刚在代码中绘制的折线图中,数据行将始终位于图表的顶部。如果你的数据在很高的水平上波动,y 的刻度从 0 开始,你就有趋势线变平的风险。当 y 轴的上限是 y 的最大值时,这也不是最佳选择。在这里,您将添加对轴范围的检查。为此,在清单 20-20 中,您定义了四个变量来指定 x 轴和 y 轴的上限和下限。
清单 20-20。ch20_03.html
var xLowLim = 0;
var xUpLim = d3.max(ax);
var yUpLim = 1.2 * d3.max(ay);
var yLowLim = 0.8 * d3.min(ay);
因此,您可以用这些变量替换所有的限制引用。请注意,代码变得更加易读。以直接的方式指定这四个限制使您能够在需要时轻松地修改它们。在这种情况下,只显示了 y 轴上实验数据覆盖的范围,加上 20%的余量,如清单 20-21 所示。
清单 20-21。ch20_03.html
y = d3.scale.linear().domain(``yLowLim, yUpLim
x = d3.scale.linear().domain([``xLowLim, xUpLim
...
//draw the x ticks
g.selectAll(".xTicks")
.data(x.ticks(5))
.enter().append("svg:line")
.attr("class", "xTicks")
.attr("x1", function(d) { return x(d); })
.attr("y1", -y(yLowLim))
.attr("x2", function(d) { return x(d); })
.attr("y2", -y(yLowLim)-5)
// draw the y ticks
g.selectAll(".yTicks")
.data(y.ticks(5))
.enter().append("svg:line")
.attr("class", "yTicks")
.attr("y1", function(d) { return -y(d); })
.attr("x1", x(xLowLim))
.attr("y2", function(d) { return -y(d); })
.attr("x2", x(xLowLim)+5)
//draw the x grid
g.selectAll(".xGrids")
.data(x.ticks(5))
.enter().append("svg:line")
.attr("class", "xGrids")
.attr("x1", function(d) { return x(d); })
.attr("y1", -y(yLowLim))
.attr("x2", function(d) { return x(d); })
.attr("y2", -y(yUpLim))
// draw the y grid
g.selectAll(".yGrids")
.data(y.ticks(5))
.enter().append("svg:line")
.attr("class", "yGrids")
.attr("y1", function(d) { return -y(d); })
.attr("x1", x(xUpLim)+20)
.attr("y2", function(d) { return -y(d); })
.attr("x2", x(xLowLim))
// draw the x axis
g.append("svg:line")
.attr("x1", x(xLowLim))
.attr("y1", -y(yLowLim))
.attr("x2", 1.2*x(xUpLim))
.attr("y2", -y(yLowLim))
// draw the y axis
g.append("svg:line")
.attr("x1", x(xLowLim))
.attr("y1", -y(yLowLim))
.attr("x2", x(xLowLim))
.attr("y2", -1.2*y(yUpLim))
图 [20-8 显示了 y 轴范围在 60°和 160°之间的新折线图,更好地显示了线条。
图 20-8。
A line chart with y-axis range focused around the y values
添加轴箭头
为了更好地理解 D3 的图形多功能性,特别是在新特性的实现中,您将学习向 x 轴和 y 轴添加箭头。要做到这一点,你必须在清单 20-22 中添加两条路径,因为它们会在两个轴的末端画出箭头。
清单 20-22。ch20_04.html
g.append("svg:path")
.attr("class", "axisArrow")
.attr("d", function() {
var x1 = x(xUpLim)+23, x2 = x(xUpLim)+30;
var y2 = -y(yLowLim),y1 = y2-3, y3 = y2+3
return 'M'+x1+','+y1+','+x2+','+y2+','+x1+','+y3;
});
g.append("svg:path")
.attr("class", "axisArrow")
.attr("d", function() {
var y1 = -y(yUpLim)-13, y2 = -y(yUpLim)-20;
var x2 = x(xLowLim),x1 = x2-3, x3 = x2+3
return 'M'+x1+','+y1+','+x2+','+y2+','+x3+','+y1;
});
在 CCS 风格中,您添加了axisArrow
类,如清单 20-23 所示。您也可以选择启用fill
属性来获得一个实心箭头。
清单 20-23。ch20_04.html
.axisArrow {
stroke: black;
stroke-width: 1;
/*fill: black; */
}
图 20-9 显示了填充和未填充的结果。
图 20-9。
Two different ways to represent the arrows on the axes
添加标题和轴标签
在本节中,您将向图表添加标题。这是一件非常简单的事情,您将使用名为text
的 SVG 元素,并对样式进行适当的修改,如清单 20-24 所示。这段代码将把标题放在中间的顶部。
清单 20-24。ch20_05.html
g.append("svg:text")
.attr("x", (w / 2))
.attr("y", -h + margin_y )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("My first D3 line chart");
图 20-10 显示了添加到折线图顶部的标题。
图 20-10。
A line chart with a title
按照类似的步骤,你也可以给轴添加标签(见清单 20-25)。
清单 20-25。ch20_05.html
g.append("svg:text")
.attr("x", 25)
.attr("y", -h + margin_y)
.attr("text-anchor", "end")
.style("font-size", "11px")
.text("[#]");
g.append("svg:text")
.attr("x", w - 40)
.attr("y", -8 )
.attr("text-anchor", "end")
.style("font-size", "11px")
.text("time [s]");
图 20-11 显示了放在相应轴旁边的两个新轴标签。
图 20-11。
A more complete line chart with title and axes labels
现在,您已经学会了如何制作折线图,您可以尝试一些更复杂的图表了。通常,要在图表中显示的数据不在网页中,而是在外部文件中。您将把以下关于如何从外部文件读取数据的课程与您目前所学的内容结合起来。
从 CSV 文件中的数据绘制折线图
设计图表时,通常会引用各种格式的数据。这些数据通常来自几个不同的来源。在最常见的情况下,您的服务器(您的 web 页面所指向的服务器)上有从数据库或通过检测提取数据的应用,或者您甚至可能有在这些服务器上收集的数据文件。这里的示例使用位于服务器上的逗号分隔值(CSV)文件作为数据源。这个 CSV 文件包含数据,可以直接加载到服务器上,也可以由其他应用生成。
D3 已经准备好处理这种类型的文件,这不是巧合。为此,D3 提供了函数d3.csv()
。您将通过一个示例了解关于这个主题的更多信息。
读取和解析数据
首先,您需要定义“画布”的大小,或者更好地定义您想要绘制图表的区域的大小和边距。这一次,您定义了四个边距。这将使你对绘图区域有更多的控制(见清单 20-26)。
清单 20-26。ch20_06a.html
<!DOCTYPE html>
<meta charset="utf-8">
<style>
</style>
<body>
<script src="
http://d3js.org/d3.v3.js
<script>
var margin = {top: 70, right: 20, bottom: 30, left: 50},
w = 400 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
现在你处理数据;用文本编辑器将清单 20-27 中的数据写入一个文件,并另存为data_01.csv
。
清单 20-27。data_01.csv
date,attendee
12-Feb-12,80
27-Feb-12,56
02-Mar-12,42
14-Mar-12,63
30-Mar-12,64
07-Apr-12,72
18-Apr-12,65
02-May-12,80
19-May-12,76
28-May-12,66
03-Jun-12,64
18-Jun-12,53
29-Jun-12,59
该数据包含由逗号分隔的两组值(回想一下 CSV 代表逗号分隔值)。第一个是日期格式,列出发生特定事件的日期,例如会议。第二列列出了与会者的人数。请注意,日期没有用引号括起来。
以类似 jqPlot 的方式,D3 有许多控制时间格式化的工具。事实上,要处理 CSV 文件中包含的日期,您必须指定一个解析器,如清单 20-28 所示。
清单 20-28。ch20_06a.html
var parseDate = d3.time.format("%d-%b-%y").parse;
这里需要指定 CSV 文件中包含的格式:%d
表示天数的数字格式,%b
表示前三个字符表示上报的月份,%y
表示后两位数字表示上报的年份。您可以指定 x 和 y 值,给它们指定一个比例和范围,如清单 20-29 所示。
清单 20-29。ch20_06a.html
var x = d3.time.scale().range([0, w]);
var y = d3.scale.linear().range([h, 0]);
现在您已经处理了输入数据的正确处理,您可以开始创建图形组件了。
实现轴和网格
你将从学习如何图形化地实现两个笛卡尔轴开始。在这个例子中,如清单 20-30 所示,你按照最合适的方式通过函数d3.svg.axis()
指定 x 轴和 y 轴。
清单 20-30。ch20_06a.html
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5);
这使您可以专注于数据,而所有与轴相关的问题(记号、标签等)都由轴组件自动处理。因此,在您创建了xAxis
和yAxis
之后,您将 x 和 y 的比例分配给它们并设置方向。简单吗?有;这一次,您不必指定所有关于轴的繁琐内容——它们的限制,在哪里放置刻度和标签,等等。与前面的例子不同,所有这些都是用很少的几行自动完成的。我选择现在引入这个概念,因为在前面的例子中,我想强调这样一个事实,即你设计的每一个项目都是你可以用 D3 管理的一块砖,不管这个过程是否在 D3 库中自动化。
现在您可以将 SVG 元素添加到页面中,如清单 20-31 所示。
清单 20-31。ch20_06a.html
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
注意 x 轴是如何被平移的。事实上,在没有规范的情况下,x 轴将被绘制在绘图区域的顶部。而且,你还需要添加 CSS 样式。请参见清单 20-32。
清单 20-32。ch20_06a.html
<style>
body {
font: 10px verdana;
}
.axis path,
.axis line {
fill: none;
stroke: #333;
}
</style>
图 20-12 显示了结果。
图 20-12。
An empty chart ready to be filled with data
使用 FireBug,您可以看到刚刚定义的 SVG 元素的结构(参见图 20-13 )。
图 20-13。
FireBug shows the structure of the SVG elements created dynamically to display the axes
您可以看到所有的元素都被自动分组到组<g>
标签中。这使您能够更好地将可能的转换应用到单独的元素。
如果需要,您也可以添加网格。你建立网格的方法和建立轴的方法一样。事实上,以同样的方式,您使用清单 20-33 中的axis()
函数定义了两个网格变量——xGrid
和yGrid
。
清单 20-33。ch20_06a.html
var yAxis = d3.svg.axis()
...
var xGrid = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5)
.tickSize(-h, 0, 0)
.tickFormat("");
var yGrid = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.tickSize(-w, 0, 0)
.tickFormat("");
在 JavaScript 代码的底部,将两个新的 SVG 元素添加到另一个中,如清单 20-34 所示。
清单 20-34。ch20_06a.html
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
svg.append("g")
.attr("class", "grid")
.attr("transform", "translate(0," + h + ")")
.call(xGrid);
svg.append("g")
.attr("class", "grid")
.call(yGrid);
这两个元素使用相同的类名命名:grid
。因此,你可以把它们作为一个单独的元素来设计(见清单 20-35)。
清单 20-35。ch20_06a.html
<style>
...
.grid .tick {
stroke: lightgrey;
opacity: 0.7;
}
.grid path {
stroke-width: 0;
}
</style>
图 20-14 显示了您刚刚定义为 SVG 元素的水平网格线。
图 20-14。
Beginning to draw the horizontal grid lines
您的图表现在可以显示 CSV 文件中的数据了。
用 csv()函数绘制数据
现在是时候在图表中显示数据了,你可以用 D3 函数d3.csv()
来完成,如清单 20-36 所示。
清单 20-36。ch20_06a.html
d3.csv("data_01.csv", function(error, data) {
// Here we will put all the SVG elements affected by the data
// on the file!!!
});
第一个参数是 CSV 文件的名称;第二个参数是处理文件中所有数据的函数。所有以某种方式受这些值影响的 D3 函数都必须放在这个函数中。例如,您使用svg.append()
来创建新的 SVG 元素,但是其中许多函数需要知道数据的 x 和 y 值。所以你需要把它们放在csv()
函数中作为第二个参数。
CSV 文件中的所有数据都收集在一个名为data
的对象中。CSV 文件的不同字段通过它们的标题来识别。您要添加的第一件事是一个迭代函数,其中的data
对象被逐项读取。在这里,解析日期值。您必须确保所有与会者值都被读取为数字(这可以通过在每个值前面加上一个加号来实现)。请参见清单 20-37。
清单 20-37。ch20_06a.html
d3.csv("data_01.csv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.attendee = +d.attendee;
});
});
只有现在才有可能在清单 20-38 中定义 x 和 y 上的定义域,因为只有现在你才知道这些数据的值。
清单 20-38。ch20_06a.html
d3.csv("data_01.csv", function(error, data) {
data.forEach(函数(d) {
...
});
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain(d3.extent(data, function(d) { return d.attendee; }));
});
一旦从文件中读取并收集了数据,它就构成了一组必须用线连接的点(x,y)。您将使用 SVG 元素path
来构建这一行,如清单 20-39 所示。正如您之前看到的,函数d3.svg.line()
使工作变得更加容易。
清单 20-39。ch20_06a.html
d3.csv("data_01.csv", function(error, data) {
data.forEach(函数(d) {
...
});
...
var line = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.attendee); });
});
您还可以向图表添加两个轴标签和一个标题。这是一个如何手动构建<g>
组的好例子。以前,组和其中的所有元素都是由函数创建的;现在你需要明确地做到这一点。如果你想给一个组添加两个轴标签,给另一个组添加标题,你需要指定两个不同的变量:labels
和title
(见清单 20-40)。
清单 20-40。ch20_06a.html
d3.csv("data_01.csv", function(error, data) {
data.forEach(函数(d) {
...
});
...
var labels = svg.append("g")
.attr("class","labels")
labels.append("text")
.attr("transform", "translate(0," + h + ")")
.attr("x", (w-margin.right))
.attr("dx", "-1.0em")
.attr("dy", "2.0em")
.text("[Months]");
labels.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -40)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Attendees");
var title = svg.append("g")
.attr("class","title");
title.append("text")
.attr("x", (w / 2))
.attr("y", -30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("A D3 line chart from CSV file");
});
在每种情况下,都用append()
方法创建一个 SVG 元素<g>
,并用一个类名定义这个组。随后,通过对这些变量使用append()
,将 SVG 元素分配给这两个组。
最后,你可以添加path
元素,它画出代表数据值的线(见清单 20-41)。
清单 20-41。ch20_06a.html
d3.csv("data_01.csv", function(error, data) {
data.forEach(function(d) {
...
});
...
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
});
即使对于这个新的 SVG 元素,您也不能忘记添加它的 CSS 样式设置,如清单 20-42 所示。
清单 20-42。ch20_06a.html
<style>
...
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
</style>
图 20-15 显示了报告 CSV 文件中所有数据的漂亮折线图。
图 20-15。
A complete line chart with all of its main components
向线添加标记
正如您在 jqPlot 的折线图中看到的,即使在这里也可以进行进一步的添加。例如,您可以在线上放置数据标记。
在所有添加的 SVG 元素末尾的d3.csv()
函数中,你可以添加标记(见清单 20-43)。记住这些元素依赖于数据,所以它们必须被插入到csv()
函数中。
清单 20-43。ch20_06b.html
d3.csv("data_01.csv", function(error, data) {
data.forEach(函数(d) {
...
});
...
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", function(d) { return x(d.date); })
.attr("cy", function(d) { return y(d.attendee); });
});
在文件的样式部分,添加清单 20-44 中的.dot
类的 CSS 样式定义。
清单 20-44。ch20_06b.html
.dot {
stroke: steelblue;
fill: lightblue;
}
图 20-16 为小圆圈标记的折线图;这个结果与用 jqPlot 库得到的结果非常相似。
图 20-16。
A complete line chart with markers
这些标记是圆形的,但是也可以是其他的形状和颜色。例如,你可以使用正方形的标记(见清单 20-45)。
清单 20-45。ch20_06c.html
<style>
.dot {
stroke: darkred;
fill: red;
}
</style>
...
svg.selectAll(".dot")
.data(data)
.enter().append("rect")
.attr("class", "dot")
.attr("width", 7)
.attr("height", 7)
.attr("x", function(d) { return x(d.date)-3.5; })
.attr("y", function(d) { return y(d.attendee)-3.5; });
图 20-17 显示了相同的折线图,但这次它使用红色小方块作为标记。
图 20-17。
One of the many marker options
你也可以使用黄色菱形的标记,通常被称为菱形(见清单 20-46)。
清单 20-46。ch20_06d.html
<style>
.dot {
stroke: orange;
fill: yellow;
}
</style>
...
svg.selectAll(".dot")
.data(data)
.enter().append("rect")
.attr("class", "dot")
.attr("transform", function(d) {
var str = "rotate(45," + x(d.date) + "," + y(d.attendee) + ")";
return str;
})
.attr("width", 7)
.attr("height", 7)
.attr("x", function(d) { return x(d.date)-3.5; })
.attr("y", function(d) { return y(d.attendee)-3.5; });
图 20-18 显示了黄色菱形的标记。
图 20-18。
Another marker option
带填充区域的折线图
在本节中,您将把点标记放在一边,并返回到基本折线图。您可以添加到图表中的另一个有趣的功能是填充线条下方的区域。还记得d3.svg.line()
功能吗?嗯,这里你用的是d3.svg.area()
函数。就像 D3 里有一个line
对象一样,你也有一个area
对象。因此,要定义一个area
对象,你可以将清单 20-47 中粗体显示的行添加到代码中,就在line
对象定义的下面。
清单 20-47。ch20_07.html
var line = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.attendee); });
var area = d3.svg.area()
.x(function(d) { return x(d.date); })
.y0(h)
.y1(function(d) { return y(d.attendee); });
var labels = svg.append("g")
...
如您所见,要定义一个区域,您需要指定三个界定边缘的函数:x
、y0
和y1
。在这种情况下,y0
是常数,对应于绘图区域的底部(x 轴)。现在您需要在 SVG 中创建相应的元素,它由一个path
元素表示,如清单 20-48 所示。
清单 20-48。ch20_07.html
d3.csv("data_01.csv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.attendee = +d.attendee;
});
...
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
svg.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area);
});
如清单 20-49 所示,您需要在相应的 CSS 样式类中指定颜色设置。
清单 20-49。ch20_07.html
.area {
fill: lightblue;
}
图 20-19 显示了从折线图派生的面积图。
图 20-19。
An area chart
多系列折线图
现在您已经熟悉了使用 SVG 元素创建折线图的基本组件,下一步是开始处理多个数据系列:多系列折线图。本节中最重要的元素是图例。您将学习通过利用 SVG 提供的基本图形元素来创建一个。
处理多个系列的数据
到目前为止,您一直在处理单个系列的数据。现在是转向多系列的时候了。在前面的示例中,您使用 CSV 文件作为数据源。现在,你将看到另一个 D3 函数:d3.tsv()
。它执行与csv()
相同的任务,但是操作制表符分隔值(TSV)文件。
将清单 20-50 复制到您的文本编辑器中,并保存为data_02.tsv
(参见下面的注释)。
Note
TSV 文件中的值是用制表符分隔的,所以当您编写或复制清单 20-50 时,记得检查每个值之间只有一个制表符。
清单 20-50。data_02.tsv
Date europa asia america
12-Feb-12 52 40 65
27-Feb-12 56 35 70
02-Mar-12 51 45 62
14-Mar-12 63 44 82
30-Mar-12 64 54 85
07-Apr-12 70 34 72
18-Apr-12 65 36 69
02-May-12 56 40 71
19-May-12 71 55 75
28-May-12 45 32 68
03-Jun-12 64 44 75
18-Jun-12 53 36 78
29-Jun-12 59 42 79
清单 20-50 有四列,其中第一列是日期,另外三列是来自不同大洲的值。第一列包含 x 值;其他的是三个系列对应的 y 值。
开始编写清单 20-51 中的代码;没有任何解释,因为这段代码实际上与上一个示例相同。
清单 20-51。ch20_08a.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="
http://d3js.org/d3.v3.js
<style>
body {
font: 10px verdana;
}
.axis path,
.axis line {
fill: none;
stroke: #333;
}
.grid .tick {
stroke: lightgrey;
opacity: 0.7;
}
.grid path {
stroke-width: 0;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
</style>
</head>
<body>
<script type="text/javascript">
var margin = {top: 70, right: 20, bottom: 30, left: 50},
w = 400 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var parseDate = d3.time.format("%d-%b-%y").parse;
var x = d3.time.scale().range([0, w]);
var y = d3.scale.linear().range([h, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5);
var xGrid = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5)
.tickSize(-h, 0, 0)
.tickFormat("");
var yGrid = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.tickSize(-w, 0, 0)
.tickFormat("");
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var line = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.attendee); });
// Here we add the d3.tsv function
// start of the part of code to include in the d3.tsv() function
d3.tsv("data_02.tsv ",函数(error,data) {
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "grid")
.attr("transform", "translate(0," + h + ")")
.call(xGrid);
svg.append("g")
.attr("class", "grid")
.call(yGrid);
});
//end of the part of code to include in the d3.tsv() function
var labels = svg.append("g")
.attr("class","labels");
labels.append("text")
.attr("transform", "translate(0," + h + ")")
.attr("x", (w-margin.right))
.attr("dx", "-1.0em")
.attr("dy", "2.0em")
.text("[Months]");
labels.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -40)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Attendees");
var title = svg.append("g")
.attr("class","title");
title.append("text")
.attr("x", (w / 2))
.attr("y", -30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("A multiseries line chart");
</script>
</body>
</html>
当您在单个图表中处理多系列数据时,您需要能够快速识别数据,因此您需要使用不同的颜色。D3 提供了一些生成已经定义的颜色序列的函数。例如,有一个category10()
函数,它提供了 10 种不同颜色的序列。您可以通过编写清单 20-52 中的线条来为多系列折线图创建一个颜色集。
清单 20-52。ch20_08a.html
...
var x = d3.time.scale().range([0, w]);
var y = d3.scale.linear().range([h, 0]);
var color = d3.scale.category10();
var xAxis = d3.svg.axis()
...
您现在需要读取 TSV 文件中的数据。和前面的例子一样,在调用了d3.tsv()
函数之后,你添加了一个解析器,如清单 20-53 所示。因为必须处理 x 轴上的日期值,所以必须解析这种类型的值。您将使用parseDate()
函数。
清单 20-53。ch20_08a.html
d3.tsv("data_02.tsv ",函数(error,data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
});
...
});
您已经定义了一个颜色集,在带有scale()
函数的链中使用了category10()
函数。这意味着 D3 将颜色序列作为一个标度来处理。您需要创建一个域,如清单 20-54 所示(在这种情况下,它将由离散值组成,而不是像 x 或 y 那样的连续值)。该域由 TSV 文件中的头组成。在这个例子中,你有三块大陆。因此,您将拥有一个包含三个值的属性域和一个包含三种颜色的颜色序列。
清单 20-54。ch20_08a.html
d3.tsv("data_02.tsv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
});
color.domain(d3.keys(data[0]).filter(function(key) {
return key !== "date";
}));
...
});
在清单 20-53 中,你可以看到data[0]
被作为参数传递给了d3.keys()
函数。data[0]
是 TSV 文件第一行对应的对象:
Object { date=Date {Sun Feb 12 2012 00:00:00 GMT+0100},
europa="52", asia="40", america="65"}.
d3.keys()
函数从一个对象中提取值的名称,这个名称就是我们在 TSV 文件中发现的标题。所以使用d3.keys(data[0])
,你得到了字符串数组:
["date","europa","asia","america"]
您只对最后三个值感兴趣,所以您需要过滤这个数组,以便排除键"date".
,您可以使用filter()
函数来这样做。最后,您将把三大洲指定给颜色域。
["europa","asia","america"]
清单 20-55 中的命令重组了结构化对象数组中的所有数据。这是由带有内部函数的函数map()
完成的,它按照定义的结构映射值。
清单 20-55。ch20_08a.html
d3.tsv("data_02.tsv", function(error, data) {
...
color.domain(d3.keys(data[0]).filter(function(key) {
return key !== "date";
}));
var continents = color.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {date: d.date, attendee: +d[name]};
})
};
});
...
});
这是由三个物体组成的阵列,叫做大陆。
[ Object { name="europa", values=[13]},
Object { name="asia", values=[13]},
Object { name="america", values=[13]} ]
每个对象都有一个洲名和一个由 13 个对象组成的值数组:
[ Object { date=Date, attendee=52 },
Object { date=Date, attendee=56 },
Object { date=Date, attendee=51 },
...]
您以一种允许后续处理的方式组织数据。事实上,当你需要指定图表的 y 域时,你可以通过两次迭代找到系列中所有值的最大值和最小值(不是每个单独的值)(见清单 20-56)。使用function(c)
,可以对所有的大陆进行迭代,使用function(v)
,可以对其中的所有值进行迭代。最终,d3.min
和d3.max
将只提取一个值。
清单 20-56。ch20_08a.html
d3.tsv("data_02.tsv", function(error, data) {
...
var continents = color.domain().map(function(name) {
...
});
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([
d3.min(continents, function(c) {
return d3.min(c.values, function(v) { return v.attendee; });
}),
d3.max(continents, function(c) {
return d3.max(c.values, function(v) { return v.attendee; });
})
]);
...
});
由于有了新的数据结构,您可以为每个包含一条线路径的大陆添加一个 SVG 元素<g>
,如清单 20-57 所示。
清单 20-57。ch20_08a.html
d3.tsv("data_02.tsv", function(error, data) {
...
svg.append("g")
.attr("class", "grid")
.call(yGrid);
var continent = svg.selectAll(".continent")
.data(continents)
.enter().append("g")
.attr("class", "continent");
continent.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return color(d.name); });
});
由此产生的多系列折线图如图 20-20 所示。
图 20-20。
A multiseries line chart
添加图例
当您处理多序列图表时,下一个合乎逻辑的步骤是添加图例,以便用颜色和标签对序列进行分类。因为图例和其他任何图形对象一样,都是一个图形对象,所以您需要添加 SVG 元素来允许您在图表上绘制它(参见清单 20-58)。
清单 20-58。ch20_08a.html
d3.tsv("data_02.tsv", function(error, data) {
...
continent.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return color(d.name); });
var legend = svg.selectAll(".legend")
.data(color.domain().slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", w - 18)
.attr("y", 4)
.attr("width", 10)
.attr("height", 10)
.style("fill", color);
legend.append("text")
.attr("x", w - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
});
产生的多系列折线图如图 20-21 所示,带有图例。
图 20-21。
A multiseries line chart with a legend
插值线
你还记得用 jqPlot 库处理多系列折线图时线条的平滑效果吗?(如果没有,可以在第九章的“平滑折线图”部分找到。)在折线图中,您通常将数据点按顺序一个接一个地连接成一条直线。您还看到了如何将这些点连接成一条曲线。事实上,这种效果是通过插值得到的。从数学的角度来看,D3 库以更正确的方式覆盖了数据点的插值。因此,你需要更深入地研究这个概念。
当您有一组值并希望用折线图来表示它们时,您实际上希望了解这些值所代表的趋势。根据这一趋势,您可以评估在一个数据点和下一个数据点之间的中间点可以获得哪些值。有了这样的估计,你实际上影响了插值。根据趋势和想要达到的精确度,您可以使用各种数学方法来调整连接数据点的曲线形状。
最常用的方法是样条。(如果你想加深对题目的了解,请访问 http://paulbourke.net/miscellaneous/interpolation/
。)表 20-1 列出了 D3 库提供的各种插值类型。
表 20-1。
The options for interpolating lines available within the D3 library
| 选择 | 描述 | | --- | --- | | `basis` | 一条 B 样条曲线,两端有重复的控制点。 | | `basis-open` | 一个开放的 B 样条;不得与起点或终点相交。 | | `basis-closed` | 闭合的 B 样条,如在循环中。 | | `bundle` | 等同于`basis`,除了张力参数用于拉直花键。 | | `cardinal` | 基数样条,两端有控制点副本。 | | `cardinal-open` | 开基数样条;可能不会与起点或终点相交,但会与其他控制点相交。 | | `cardinal-closed` | 闭合基数样条,如在环中。 | | `Linear` | 分段线性线段,如在折线中。 | | `linear-closed` | 闭合线性线段以形成多边形。 | | `monotone` | 保持 y 方向单调效果的三次插值。 | | `step-before` | 在垂直段和水平段之间交替,如在阶跃函数中。 | | `step-after` | 在水平段和垂直段之间交替,如在阶跃函数中。 |You find these options by visiting https://github.com/mbostock/d3/wiki/SVG-Shapes#wiki-line_interpolate
.
现在你更好地理解了什么是插值,你可以看到一个实际的例子。在前面的示例中,有三个系列由不同颜色的线表示,并由连接数据点(x,y)的线段组成。但是也可以绘制相应的插值线。
如清单 20-59 所示,您只需将interpolate()
方法添加到d3.svg.line
中就可以获得想要的效果。
清单 20-59。ch20_08b.html
var line = d3.svg.line()
.interpolate("basis")
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.attendee); });
图 20-22 显示了应用于图表中三个系列的插值线。连接数据点的直线已被曲线取代。
图 20-22。
A smooth multiseries line chart
差异折线图
这种图表描绘了两个系列之间的区域。在第一个系列大于第二个系列的范围内,该区域具有一种颜色,在第一个系列小于第二个系列的范围内,该区域具有不同的颜色。这种图表的一个很好的例子是比较收入和支出随时间变化的趋势。当收入大于支出时,该区域将是绿色的(通常绿色代表 OK),而当收入小于支出时,该区域是红色的(意味着不好)。将清单 20-60 中的值写入一个 TSV(或 CSV)文件,并将其命名为data_03.tsv
(见注释)。
Note
TSV 文件中的值是用制表符分隔的,所以当您编写或复制清单 20-60 时,记得检查每个值之间只有一个制表符。
清单 20-60。data_03.tsv
Date income expense
12-Feb-12 52 40
27-Feb-12 56 35
02-Mar-12 31 45
14-Mar-12 33 44
30-Mar-12 44 54
07-Apr-12 50 34
18-Apr-12 65 36
02-May-12 56 40
19-May-12 41 56
28-May-12 45 32
03-Jun-12 54 44
18-Jun-12 43 46
29-Jun-12 39 52
开始编写清单 20-61 中的代码;这次不包括解释,因为这个例子实际上与上一个一样。
清单 20-61。ch20_09.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="
http://d3js.org/d3.v3.js
<style>
body {
font: 10px verdana;
}
.axis path,
.axis line {
fill: none;
stroke: #333;
}
.grid .tick {
stroke: lightgrey;
opacity: 0.7;
}
.grid path {
stroke-width: 0;
}
</style>
</head>
<body>
<script type="text/javascript">
var margin = {top: 70, right: 20, bottom: 30, left: 50},
w = 400 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var parseDate = d3.time.format("%d-%b-%y").parse;
var x = d3.time.scale().range([0, w]);
var y = d3.scale.linear().range([h, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5);
var xGrid = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5)
.tickSize(-h, 0, 0)
.tickFormat("");
var yGrid = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.tickSize(-w, 0, 0)
.tickFormat("");
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Here we add the d3.tsv function
// start of the part of code to include in the d3.tsv() function
d3.tsv("data_03.tsv", function(error, data) {
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "grid")
.attr("transform", "translate(0," + h + ")")
.call(xGrid);
svg.append("g")
.attr("class", "grid")
.call(yGrid);
});
//end of the part of code to include in the d3.tsv() function
var labels = svg.append("g")
.attr("class","labels");
labels.append("text")
.attr("transform", "translate(0," + h + ")")
.attr("x", (w-margin.right))
.attr("dx", "-1.0em")
.attr("dy", "2.0em")
.text("[Months]");
labels.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -40)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Millions ($)");
var title = svg.append("g")
.attr("class","title");
title.append("text")
.attr("x", (w / 2))
.attr("y", -30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("A difference chart");
</script>
</body>
</html>
首先,您阅读 TSV 文件,检查收入和费用值是否为正。然后解析所有的日期值(见清单 20-62)。
清单 20-62。ch20_09.html
d3.tsv("data_03.tsv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.income = +d.income;
d.expense = +d.expense;
});
...
});
这里,不像前面的例子(多系列折线图),不需要重新构造数据,所以你可以在 x 和 y 上创建一个域,如清单 20-63 所示。用Math.max
和Math.min
比较每一步的收入和费用值,然后用d3.min
和d3.max
找出影响每一步迭代的值,得到最大值和最小值。
清单 20-63。ch20_09.html
d3.tsv("data_03.tsv", function(error, data) {
data.forEach(function(d) {
...
});
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([
d3.min(data, function(d) {return Math.min(d.income, d.expense); }),
d3.max(data, function(d) {return Math.max(d.income, d.expense); })
]);
...
});
在添加 SVG 元素之前,您需要定义一些 CSS 类。当支出大于收入时,你会用红色,否则用绿色。你需要定义这些颜色,如清单 20-64 所示。
清单 20-64。ch20_09.html
<style>
...
.area.above {
fill: darkred;
}
.area.below {
fill: lightgreen;
}
.line {
fill: none;
stroke: #000;
stroke-width: 1.5px;
}
</style>
因为你需要表示线条和区域,你可以通过数据点之间的插值来定义它们(见清单 20-65)。
清单 20-65。ch20_09.html
d3.tsv("data_03.tsv", function(error, data) {
...
svg.append("g")
.attr("class", "grid")
.call(yGrid);
var line = d3.svg.area()
.interpolate("basis")
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d["income"]); });
var area = d3.svg.area()
.interpolate("basis")
.x(function(d) { return x(d.date); })
.y1(function(d) { return y(d["income"]); });
});
如你所见,你实际上只定义了收入点的线;没有对费用值的引用。但是你对收支两条线之间的区域感兴趣,所以当你定义path
元素时,为了画出这个区域,你可以把费用值作为一个边界,用一个通用函数迭代d
值(见清单 20-66)。
清单 20-66。ch20_09.html
d3.tsv("data_03.tsv", function(error, data) {
...
var area = d3.svg.area()
.interpolate("basis")
.x(function(d) { return x(d.date); })
.y1(function(d) { return y(d["income"]); });
svg.datum(data);
svg.append("path")
.attr("class", "area below")
.attr("d", area.y0(function(d) { return y(d.expense); }));
svg.append("path")
.attr("class", "line")
.attr("d", line);
});
如果你现在加载网页,你应该得到想要的区域(见图 20-23 )。
图 20-23。
An initial representation of the area between both trends
但是所有的区域都是绿色的。相反,你希望其中一些区域是红色的。您需要选择收入线和费用线所包围的区域,其中收入线在费用线之上,并排除与此方案不对应的区域。当您处理区域时,必须增加或减少部分区域,有必要引入裁剪路径 SVG 元素。
剪辑路径是 SVG 元素,可以用path
元素附加到以前绘制的图形上。剪辑路径描述了一个“窗口”区域,它只显示在由路径定义的区域中。图形的其他区域保持隐藏。
看一下图 20-24 。你可以看到收入线又黑又粗。这条线以上的所有绿色区域(在印刷书籍版本中为浅灰色)应该被裁剪路径隐藏。但是你需要什么样的剪辑路径呢?您需要由界定收入线上方较低区域的路径描述的剪辑路径。
图 20-24。
Selection of the positive area with a clip path area
你需要对代码做一些修改,如清单 20-67 所示。
清单 20-67。ch20_09.html
d3.tsv("data_03.tsv", function(error, data) {
...
svg.datum(data);
svg.append("clipPath")
.attr("id", "clip-below")
.append("path")
.attr("d", area.y0(h));
svg.append("path")
.attr("class", "area below")
.attr("clip-path", "url(#clip-below)")
.attr("d", area.y0(function(d) { return y(d.expense); }));
svg.append("path")
.attr("class", "line")
.attr("d", line);
});
现在你需要对红色区域做同样的事情(在印刷书籍版本中是深灰色)。总是从收入线和支出线之间的区域开始,您必须消除收入线以下的区域。所以,如图 20-25 所示,可以使用描述收入线以上区域的裁剪路径作为窗口区域。
图 20-25。
Selection of the negative area with a clip path area
将它转换成代码,你需要在代码中添加另一个clipPath
,如清单 20-68 所示。
清单 20-68。ch20_09.html
d3.tsv("data_03.tsv", function(error, data) {
...
svg.append("path")
.attr("class", "area below")
.attr("clip-path", "url(#clip-below)")
.attr("d", area.y0(function(d) { return y(d.expense); }));
svg.append("clipPath")
.attr("id", "clip-above")
.append("path")
.attr("d", area.y0(0));
svg.append("path")
.attr("class", "area above")
.attr("clip-path", "url(#clip-above)")
.attr("d", area.y0(function(d) { return y(d.expense); }));
svg.append("path")
.attr("class", "line")
.attr("d", line);
});
最终,两个区域同时被绘制,你得到了想要的图表(见图 20-26 )。
图 20-26。
The final representation of the difference area chart
摘要
本章展示了如何构建折线图的基本元素,包括轴、轴标签、标题和网格。特别是,你已经了解了标度、定义域和范围的概念。
然后,您学习了如何从外部文件中读取数据,尤其是 CSV 和 TSV 文件。此外,在开始处理多系列数据时,您学习了如何实现多系列折线图,包括学习完成它们所需的所有元素,例如图例。
最后,您学习了如何创建一种特殊类型的折线图:差异折线图。这有助于您理解剪辑区域路径。
在下一章,你将处理条形图。利用到目前为止您所学到的关于 D3 的知识,您将会看到,仅仅使用 SVG 元素,就可以实现构建条形图所需的所有图形组件。更具体地说,您将看到如何使用相同的技术实现所有可能类型的多系列条形图,从堆叠条形图到分组条形图,包括水平和垂直方向的条形图。
二十一、D3 条形图
Abstract
在这一章中,你将看到如何使用 D3 库来构建最常用的图表类型:条形图。作为第一个例子,您将从一个简单的条形图开始练习使用标量矢量图形(SVG)元素实现所有组件。
在这一章中,你将看到如何使用 D3 库来构建最常用的图表类型:条形图。作为第一个例子,您将从一个简单的条形图开始练习使用标量矢量图形(SVG)元素实现所有组件。
绘制条形图
在这方面,作为一个例子,我们选择用竖线来表示一些国家的收入,这样我们就可以对它们进行比较。作为类别标签,您将使用国家本身的名称。在这里,正如您对折线图所做的那样,您决定使用一个外部文件,比如包含所有数据的逗号分隔值(CSV)文件。然后,您的 web 页面将使用d3.csv()
函数读取文件中包含的数据。因此,将清单 21-1 中的数据写入一个文件,并保存为data_04.csv
。
清单 21-1。data_04.csv
country,income
France,14
Russia,22
Japan,13
South Korea,34
Argentina,28
清单 21-2 显示了一个空白网页,作为开发条形图的起点。你必须记得在网页中包含 D3 库(更多信息见附录 A)。如果您更喜欢使用内容交付网络(CDN)服务,您可以用以下内容替换参考:
<script src="
http://d3js.org/d3.v3.min.js
清单 21-2。ch21_01.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="../src/d3.v3.js"></script>
</head>
<body>
<script type="text/javascript">
// add the D3 code here
</script>
</body>
</html>
首先,定义希望在其上表示条形图的绘图区域的大小是一个好习惯。尺寸是由w
和h
(宽度和高度)变量指定的,但是你也必须考虑边距的空间。这些空白值必须从w
和h
中减去,适当地限制分配给你的图表的区域(见清单 21-3)。
清单 21-3。ch21_01.html
<script type="text/javascript">
var margin = {top: 70, right: 20, bottom: 30, left: 40},
w = 500 - margin.left - margin.right,
h = 350 - margin.top - margin.bottom;
var color = d3.scale.category10();
</script>
此外,如果您查看 CSV 文件中的数据(见清单 21-1),您会发现一系列五个国家及其相对值。如果你想用一种颜色来区分每个国家,就必须定义一个色标,正如你已经看到的,这可以用category10()
函数来完成。
下一步是在 x 轴和 y 轴上定义一个刻度。x 轴上没有数值,而是标识原产国的字符串值。因此,对于这种类型的值,你必须定义一个顺序标度,如清单 21-4 所示。事实上,函数rangeRoundBands
将作为参数传递的范围划分为离散的带,这正是您在条形图中需要的。对于 y 轴,因为它用数值表示变量,所以只需选择一个线性刻度。
清单 21-4。ch21_01.html
<script type="text/javascript">
...
var color = d3.scale.category10();
var x = d3.scale.ordinal()
.rangeRoundBands([0, w], .1);
var y = d3.scale.linear()
.range([h, 0]);
<script type="text/javascript">
现在您需要使用d3.svg.axis()
功能将两个刻度分配给相应的轴。当您处理条形图时,y 轴上报告的值不是标称值,而是它们占总值的百分比,这种情况并不罕见。所以,你可以通过d3.format()
定义一个百分比格式,然后通过tickFormat()
函数将它分配给 y 轴上的刻度标签(见清单 21-5)。
清单 21-5。ch21_01.html
<script type="text/javascript">
...
var y = d3.scale.linear()
.range([h, 0]);
var formatPercent = d3.format(".0%");
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(formatPercent);
<script type="text/javascript">
最后,是时候开始在 web 页面中创建 SVG 元素了。从清单 21-6 所示的根目录开始。
清单 21-6。ch21_01.html
<script type="text/javascript">
...
var yAxis = d3.svg.axis()
.scale(y)
。orient("left")
.tickFormat(formatPercent);
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
<script type="text/javascript">
现在,要访问包含在 CSV 文件中的值,你必须使用d3.csv()
函数,就像你已经做的那样,传递文件名作为第一个参数,传递包含在其中的数据的迭代函数作为第二个参数(见清单 21-7)。
清单 21-7。ch21_01.html
<script type="text/javascript">
...
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("data_04.csv", function(error, data) {
var sum = 0;
data.forEach(function(d) {
d.income = +d.income;
sum += d.income;
});
//insert here all the svg elements depending on data in the file
});
<script type="text/javascript">
在通过forEach()
循环扫描存储在文件中的值的过程中,您要确保所有的收入值都被读取为数字值,而不是字符串:这可以通过在每个值前面加上一个加号来实现。
values = +values
与此同时,你还要计算所有收入值的总和。这个总数对你计算百分比是必要的。事实上,如清单 21-8 所示,当你为两个轴定义域时,“单一收入”/总和比率被分配给 y 轴,从而得到一个百分比域。
清单 21-8。ch21_01.html
...
d3.csv("data_04.csv", function(error, data) {
data.forEach(function(d) {
...
});
x.domain(data.map(function(d) { return d.country; }));
y.domain([0, d3.max(data, function(d) { return d.income/sum; })]);
});
在设置了两个轴上的值之后,您可以添加清单 21-9 中相应的 SVG 元素来绘制它们。
清单 21-9。ch21_01.html
d3.csv("data_04.csv", function(error, data) {
...
y.domain([0, d3.max(data, function(d) { return d.income/sum; })]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
通常,对于条形图,仅在一个轴上需要网格:显示数值的轴。因为你正在处理一个垂直条形图,所以你只在 y 轴上画网格线(见清单 21-10)。另一方面,x 轴上的网格线不是必需的,因为在离散值区域中已经有了一种分类,通常称为类别。(即使在 x 轴上有连续的值要表示,但是,为了使条形图有意义,它们在 x 轴上的范围应该划分为区间或仓。这些值在每个时间间隔的频率由 y 轴上的条形高度表示,结果是一个直方图。)
清单 21-10。ch21_01.html
...
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(formatPercent);
var yGrid = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.tickSize(-w, 0, 0)
.tickFormat("");
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("data_04.csv", function(error, data) {
...
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "grid")
.call(yGrid);
});
由于网格仅位于 y 轴上,同样的事情也适用于相应的轴标签。为了以某种方式将图表的组件分开,最好为每个 SVG 元素定义一个变量<g>
,它标识一个图表组件,通常与您用来标识组件类的名称相同。因此,正如您定义一个labels
变量一样,您也定义了一个title
变量。对于您打算添加的所有其他组件,以此类推。
与 jqPlot 不同,不需要包含特定的插件来旋转轴标签;相反,使用 SVG 提供的一种可能的变换,更具体地说是旋转。您唯一需要做的事情就是传递想要旋转 SVG 元素的角度(以度为单位)。如果传递的值为正值,则旋转为顺时针方向。如果,就像在你的例子中,你想将轴标签与 y 轴对齐,那么你需要将它逆时针旋转 90 度:所以指定rotate(
–90)
作为转换(见清单 21-11)。关于title
元素,你把它放在你图表的顶部,在中心位置。
清单 21-11。ch21_01.html
d3.csv("data_04.csv", function(error, data) {
...
svg.append("g")
.attr("class", "grid")
.call(yGrid);
var labels = svg.append("g")
.attr("class","labels");
labels.append("text")
.attr("transform", "rotate(–90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Income [%]");
var title = svg.append("g")
.attr("class","title");
title.append("text")
.attr("x", (w / 2))
.attr("y", -30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("My first bar chart");
});
一旦您定义了所有的 SVG 组件,您一定不要忘记在清单 21-12 中指定 CSS 类的属性。
清单 21-12。ch21_01.html
<style>
body {
font: 14px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.grid .tick {
stroke: lightgrey;
opacity: 0.7;
}
.grid path {
stroke-width: 0;
}
.x.axis path {
display: none;
}
</style>
最后,添加 SVG 元素来组成你的条。因为您想要为每组数据绘制一个条形,所以这里您必须利用d3.csv()
函数中的迭代function(error,data)
函数。如清单 21-13 所示,您因此在函数链中添加了data(data)
和enter()
函数。此外,在每个attr()
函数中定义function(d)
,您可以迭代地将数据值一个接一个地分配给相应的属性。通过这种方式,您可以为属性x
、y
、height
和fill
分配不同的值(CSV 文件中的每一行一个值),这会影响每个条形的位置、颜色和大小。通过这种机制,每个.bar
元素将反映 CSV 文件中某一行包含的数据。
清单 21-13。ch21_01.html
d3.csv("data_04.csv", function(error, data) {
...
title.append("text")
.attr("x", (w / 2))
.attr("y", -30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("My first bar chart");
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.country); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.income/sum); })
.attr("height", function(d) { return h - y(d.income/sum); })
.attr("fill", function(d) { return color(d.country); });
});
最后,你所有的努力都会得到如图 21-1 所示的美丽条形图的回报。
图 21-1。
A simple bar chart
绘制堆积条形图
您已经用最简单的例子介绍了条形图,其中每个国家有许多组,并有相应的值(收入)。通常,您需要表示稍微复杂一点的数据,例如,您想要按部门划分总收入的数据。在这种情况下,您会将每个国家的收入分成不同的部分,每个部分代表一个生产部门的收入。在我们的例子中,我们使用 CSV 文件的方式与上一个例子非常相似(见清单 21-1),但是每个国家有多个值,这样你就可以使用多系列条形图。因此,用文本编辑器编写清单 21-14 中的数据,并保存为data_05.csv
。
清单 21-14。data_05.csv
Country,Electronics,Software,Mechanics
Germany,12,14,18
Italy,8,12,10
Spain,6,4,5
France,10,14,9
UK,7,11,9
查看文件的内容,您可能会注意到现在有四列。第一列仍然包含国家的名称,但是现在收入是三个,每个对应不同的生产部门:电子、软件和机械。这些标题列在标题中。
从上一个例子的代码开始,做一些修改并删除一些行,直到得到如清单 21-15 所示的代码。粗体显示的代码段是需要更改的代码段(标题和 CSV 文件),而不存在的代码段必须删除。那些直接从本节开始的人可以很容易地复制清单 21-15 中的内容。
清单 21-15。ch21_02.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="
http://d3js.org/d3.v3.js
<style>
body {
font: 14px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
</style>
</head>
<body>
<script type="text/javascript">
var color = d3.scale.category10();
var margin = {top: 70, right: 20, bottom: 30, left: 40},
w = 500 - margin.left - margin.right,
h = 350 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, w], .1);
var y = d3.scale.linear()
.range([h, 0]);
var formatPercent = d3.format(".0%");
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(formatPercent);
var yGrid = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.tickSize(-w, 0, 0)
.tickFormat("");
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("data_05.csv", function(error, data) {
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "grid")
.call(yGrid);
});
var labels = svg.append("g")
.attr("class","labels");
labels.append("text")
.attr("transform", "rotate(–90)")
.attr("x", 50)
.attr("y", -20)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Income [%]");
var title = svg.append("g")
.attr("class","title")
title.append("text")
.attr("x", (w / 2))
.attr("y", -30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("A stacked bar chart");
</script>
</body>
</html>
现在想想你想要设置的颜色和域。在前一个案例中,您用不同的颜色绘制了每个条形(国家)。事实上,你甚至可以给所有的条都同样的颜色。你的方法是一个可选的选择,主要是由于美学因素。但是,在这种情况下,需要使用一组不同的颜色来区分组成每个条形的各个部分。因此,每个条形都有一系列相同的颜色,每种颜色对应一个生产部门。一个小提示:当您需要一个图例来标识数据的各种表示时,您需要使用一系列颜色,反之亦然。您可以根据文件头定义颜色域,并通过过滤器删除第一项“国家”(参见清单 21-16)。
清单 21-16。ch21_02.html
d3.csv("data_05.csv", function(error, data) {
color.domain(d3.keys(data[0]).filter(function(key) {
return key !== "Country"; }));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
...
在 y 轴上,你不能画出收入的数值,而是它们占总收入的百分比。为了做到这一点,您需要知道所有收入值的总和,因此通过迭代从文件中读取的所有数据,您可以获得总和。同样,为了让 D3 明白三列(电子、机械和软件)中的值是数值,您必须在迭代中以如下方式明确指定它们:
values = +values;
在清单 21-17 中,你可以看到forEach()
函数是如何迭代文件的值的,同时,计算出你需要得到的百分比的总和。
清单 21-17。ch21_02.html
d3.csv("data_05.csv", function(error, data) {
color.domain(d3.keys(data[0]).filter(function(key) {
return key !== "Country"; }));
var sum= 0;
data.forEach(function(d){
d.Electronics = +d.Electronics;
d.Mechanics = +d.Mechanics;
d.Software = +d.Software;
sum = sum +d.Electronics +d.Mechanics +d.Software;
});
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
...
现在你需要创建一个数据结构来满足你的需求。为每个条形构建一个对象数组,其中每个对象对应于总收入被划分的部分之一。将这个数组命名为"countries”
,并通过一个迭代函数来创建它(见清单 21-18)。
清单 21-18。ch21_02.html
d3.csv("data_05.csv", function(error, data) {
...
data.forEach(function(d){
d.Electronics = +d.Electronics;
d.Mechanics = +d.Mechanics;
d.Software = +d.Software;
sum = sum +d.Electronics +d.Mechanics +d.Software;
});
data.forEach(function(d) {
var y0 = 0;
d.countries = color.domain().map(function(name) {
return {name: name, y0: y0/sum, y1: (y0 += +d[name])/sum }; });
d.total = d.countries[d.countries.length - 1].y1;
});
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
...
使用 Firebug 控制台(参见第一章中的“Firebug 和 DevTool”一节),你可以直接看到这个数组的内部结构。因此,将对控制台的调用(临时)添加到将countries
数组作为参数传递的代码中,如清单 21-19 所示。
清单 21-19。ch21_02.html
data.forEach(function(d) {
var y0 = 0;
d.countries = color.domain().map(function(name) {
return {name: name, y0: y0/sum, y1: (y0 += +d[name])/sum }; });
d.total = d.countries[d.countries.length - 1].y1;
console.log(d.countries);
});
图 21-2 显示了countries
数组的内部结构及其所有内容,以及 Firebug 控制台是如何显示的。
图 21-2。
The Firebug console shows the content and the structure of the countries array
如果您详细分析数组的第一个元素:
[Object { name="Electronics", y0=0, y1=0.08053691275167785},
Object { name="Software", y0=0.08053691275167785, y1=0.174496644295302},
Object { name="Mechanics", y0=0.174496644295302, y1=0.2953020134228188}]
您可能会注意到,数组的每个元素依次是一个包含三个对象的数组。这三个对象代表要将数据拆分成的三个类别(多系列条形图的三个系列)。y0
和y1
的值分别是条形中每个部分的开始和结束的百分比。
在你整理好所有你需要的数据后,你可以把它包含在 x 和 y 的域中,如清单 21-20 所示。
清单 21-20。ch21_02.html
d3.csv("data_05.csv", function(error, data) {
...
data.forEach(function(d) {
...
console.log(d.countries);
});
x.domain(data.map(function(d) { return d.Country; }));
y.domain([0, d3.max(data, function(d) { return d.total; })]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
...
然后,在清单 21-21 中,你开始定义构成图表条的rect
元素。
清单 21-21。ch21_02.html
d3.csv("data_05.csv", function(error, data) {
...
svg.append("g")
.attr("class", "grid")
.call(yGrid);
var country = svg.selectAll(".country")
.data(data)
.enter().append("g")
.attr("class", "country")
.attr("transform", function(d) {
return "translate(" + x(d.Country) + ",0)"; });
country.selectAll("rect")
.data(function(d) { return d.countries; })
.enter().append("rect")
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.y1); })
.attr("height", function(d) { return (y(d.y0) - y(d.y1)); })
.style("fill", function(d) { return color(d.name); });
});
您已经看到了带有库的数组的内部结构,由于 D3 总是从基本图形开始,所以最复杂的部分在于将数据结构转换成 SVG 元素的层次结构。标签有助于您建立适当的层次分组。在这方面,您必须为每个国家定义一个元素<g>
。首先,您需要迭代地使用从 CSV 文件中读取的数据。这可以通过将data
数组(原来的data
数组,而不是您刚刚定义的countries
数组)作为参数传递给data()
函数来实现。当所有这些完成后,您有五个新的组项目<g>
,因为五个是 CSV 文件中列出的国家,五个也是将要绘制的条。还应该管理每个条形在 x 轴上的位置。您不需要做任何计算来将正确的 x 值传递给translate(x,0)
函数。事实上,如图 21-3 所示,这些值是由 D3 自动生成的,利用了您已经在 x 轴上定义了一个序数刻度的事实。
图 21-3。
Firebug shows the different translation values on the x axis that are automatically generated by the D3 library
在每个组元素<g>
中,您现在必须创建<rect>
元素,这将为每个部分生成彩色矩形。此外,有必要确保将正确的值分配给y
和height
属性,以便正确地将矩形一个放置在另一个之上,避免它们重叠,从而为每个国家获得单个堆叠条形图。
这一次,将使用countries
数组,将其作为参数传递给data()
函数。由于有必要对您创建的每个元素<g>
进行进一步的迭代,您将把迭代function(d)
作为参数传递给data()
函数。这样,你在另一个迭代中创建一个迭代:第一个扫描data
(countries)中的值;第二个内部函数扫描countries
数组中的值(生产部门)。因此,您将最终百分比(y1)分配给y
属性,并将初始百分比和最终百分比之间的差值(y0–y1)分配给height
属性。当您逐个定义包含在国家数组中的对象时,值 y0 和 y1 已经在前面计算过了(参见图 21-4 )。
图 21-4。
Firebug shows the different height values attributed to each rect element
最后,您可以欣赏图 21-5 中的堆积条形图。
图 21-5。
A stacked bar chart
看着你的堆积条形图,你马上会发现少了点什么。你如何识别生产部门,他们的参考色是什么?为什么不加个图例?
正如您对其他图表组件所做的那样,您可能更喜欢为这个新组件定义一个legend
变量。一旦创建了组元素<g>
,图例也需要一次迭代(见清单 21-22)。迭代必须在生产部门进行。对于每个项目,您需要获取扇区的名称和相应的颜色。为此,这次您将利用您之前定义的颜色域:对于text
元素,您将使用 CSV 文件中的标题,而对于颜色,您将直接分配域的值。
清单 21-22。ch21_02.html
d3.csv("data_05.csv", function(error, data) {
...
country.selectAll("rect")
.data(function(d) { return d.countries; })
.enter().append("rect")
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.y1); })
.attr("height", function(d) { return (y(d.y0) - y(d.y1)); })
.style("fill", function(d) { return color(d.name); });
var legend = svg.selectAll(".legend")
.data(color.domain().slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", w - 18)
.attr("y", 4)
.attr("width", 10)
.attr("height", 10)
.style("fill", color);
legend.append("text")
.attr("x", w - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
});
为了完成堆叠条形图的主题,使用 D3 库,通过添加清单 21-23 中的单行,可以以降序来表示条形图。尽管在您的情况下您并不真正需要这个特性,但是在某些特定的情况下它可能是有用的。
清单 21-23。ch21_02.html
d3.csv("data_05.csv", function(error, data) {
...
data.forEach(function(d) {
...
console.log(d.countries);
});
data.sort(function(a, b) { return b.total - a.total; });
x.domain(data.map(function(d) { return d.Country; }));
...
});
图 21-6 显示了沿 x 轴降序排列的堆积条形图。
图 21-6。
A sorted stacked bar chart with a legend
标准化堆积条形图
在本节中,您将看到如何在规范化图表中转换前面的图表。所谓“标准化”,我们的意思是要在图表中显示的值的范围被转换成另一个目标范围,如果你谈论的是百分比,它通常从 0 到 1,或者从 0 到 100(一个非常相似的概念在第一章 9 的“范围、域和标度”一节中被处理)。因此,如果您想要比较包含彼此差异很大的数值范围的不同系列,您需要执行归一化,以 0 到 100(或 0 到 1)之间的百分比值报告所有这些区间。事实上,在比较多个数据系列时,我们通常对它们的相对特征感兴趣。例如,在我们的例子中,您可能对机械部门如何影响一个国家的经济收入(标准化)感兴趣,也可能对比较这种影响如何因国家而异(标准化值之间的比较)感兴趣。因此,为了响应这样的需求,您可以用规范化的格式表示堆积图。
您已经报告了 y 轴上的百分比值;然而,每个生产部门的百分比是相对于所有国家的收入总额计算的。这一次,百分比将根据每个国家的收入来计算。因此,在这种情况下,你不关心每个单独的部分如何分享全球收入(指所有五个国家),但你只关心每个部门在各自国家产生的收入的百分比。因此,在这种情况下,每个国家将由 100%的条形表示。现在,没有哪个国家的收入比其他国家多的信息,但是你只对每个国家内部的信息感兴趣。
所有这些推理对你来说都很重要,你要明白,尽管从相同的数据开始,你需要选择不同类型的图表,这取决于你想让那些看图表的人注意到什么。
对于这个例子,你将使用同一个文件data_05.csv
(参见清单 21-14);正如我们刚才所说的,传入的信息是相同的,但它的解释是不同的。为了规范化前面的堆积条形图,您需要对代码进行一些更改。如清单 21-24 所示,首先将左边距和右边距延长几个像素。
清单 21-24。ch21_03.html
var margin = {top: 70, right:``70``, bottom: 30, left:``50
w = 500 - margin.left - margin.right,
h = 350 - margin.top - margin.bottom;
在清单 21-25 中,在d3.csv()
函数中,你必须消除计算总收入的迭代,这是不再需要的。相反,您可以添加一个新的迭代,将每个国家的百分比考虑在内。然后,你必须消除 y 域的定义,只留下 x 域。
清单 21-25。ch21_03.html
d3.csv("data_05.csv", function(error, data) {
color.domain(d3.keys(data[0]).filter(function(key) {
return key !== "Country"; }));
data.forEach(function(d) {
var y0 = 0;
d.countries = color.domain().map(function(name) {
return {name: name, y0: y0, y1: y0 += +d[name]}; });
d.countries.forEach(function(d) { d.y0 /= y0; d.y1 /= y0; });
});
x.domain(data.map(function(d) { return d.Country; }));
var country = svg.selectAll(".country")
...
在这种新型图表中,y 标签将被条形覆盖。因此,您必须删除或注释掉rotate()
函数,以使它再次可见,如清单 21-26 所示。
清单 21-26。ch21_03.html
labels.append("text")
//.attr("transform", "rotate(–90)")
.attr("x", 50)
.attr("y", -20)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Income [%]");
当你在做的时候,为什么不抓住机会改变你的图表的标题呢?因此,修改标题,如清单 21-27 所示。
清单 21-27。ch21_03.html
title.append("text")
.attr("x", (w / 2))
.attr("y", -30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("A normalized stacked bar chart");
甚至不再需要图例。事实上,你可以用另一种功能非常相似的图形来代替它。因此,您可以从代码中删除定义清单 21-28 中图例的行。
清单 21-28。ch21_03.html
var legend = svg.selectAll(".legend")
.data(color.domain().slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", w - 18)
.attr("y", 4)
.attr("width", 10)
.attr("height", 10)
.style("fill", color);
legend.append("text")
.attr("x", w - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
现在您已经删除了图例并做了正确的更改,如果您加载网页,您将得到图 21-7 中的标准化堆积条形图。
图 21-7。
A normalized stacked bar chart
如果没有图例,你必须再一次知道,以某种方式,条形中的颜色指的是什么;您将使用报告组名称的标签来标记右边的最后一个条形。
首先在清单 21-29 中添加一个新的样式类。
清单 21-29。ch21_03.html
<style>
...
.x.axis path {
display: none;
}
.legend line {
stroke: #000;
shape-rendering: crispEdges;
}
</style>
因此,代替你刚刚删除的代码,如清单 21-28 所示,你添加清单 21-30 中的代码。
清单 21-30。ch21_03.html
country.selectAll("rect")
.data(function(d) { return d.countries; })
.enter().append("rect")
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.y1); })
.attr("height", function(d) { return (y(d.y0) - y(d.y1)); })
.style("fill", function(d) { return color(d.name); });
var legend = svg.select(".country:last-child")
.data(data);
legend.selectAll(".legend")
.data(function(d) { return d.countries; })
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d) {
return "translate(" + x.rangeBand()*0.9 + "," +
y((d.y0 + d.y1) / 2) + ")";
});
legend.selectAll(".legend")
.append("line")
.attr("x2", 10);
legend.selectAll(".legend")
.append("text")
.attr("x", 13)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
});
当您将标签添加到最后一个条时,定义它们的 SVG 元素必须属于与最后一个国家对应的组。因此,您使用.country: last-child
选择器来获取包含所有条的选择的最后一个元素。因此,新的图表看起来将如图 21-8 所示。
图 21-8。
A normalized stacked bar chart with labels as legend
绘制分组条形图
总是使用包含在data_05.csv
中的相同数据,您可以获得另一种表示:分组条形图。当你想关注每个生产部门的个人收入时,这种表示法是最合适的。在这种情况下,你并不关心这些部门在总收入中所占的比重。因此,百分比消失,取而代之的是写入 CSV 文件的 y 值。
清单 21-31 显示的部分代码几乎可以与前面的例子相媲美,所以我们不会详细讨论它。事实上,您将使用它作为添加其他代码片段的起点。
清单 21-31。ch21_04.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="../src/d3.v3.js"></script>
<style>
body {
font: 14px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
</style>
</head>
<body>
<script type="text/javascript">
var color = d3.scale.category10();
var margin = {top: 70, right: 70, bottom: 30, left: 50},
w = 500 - margin.left - margin.right,
h = 350 - margin.top - margin.bottom;
var yGrid = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.tickSize(-w, 0, 0)
.tickFormat("")
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("data_05.csv", function(error, data) {
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
svg.append("g")
.attr("class", "grid")
.call(yGrid);
});
</script>
</body>
</html>
为了这个特定的目的,你需要在 x 轴上定义两个不同的变量:x0
和x1
,两者都遵循一个顺序标度,如清单 21-32 所示。x0
标识所有条形组的顺序刻度,代表一个国家,而x1
是每个组内每个单个条形的顺序刻度,代表一个生产部门。
清单 21-32。ch21_04.html
var margin = {top: 70, right: 70, bottom: 30, left: 50},
w = 500 - margin.left - margin.right,
h = 350 - margin.top - margin.bottom;
var x0 = d3.scale.ordinal()
.rangeRoundBands([0, w], .1);
var x1 = d3.scale.ordinal();
var y = d3.scale.linear()
.range([h, 0]);
...
因此,在轴的定义中,你将 x0 赋给 x 轴,y 赋给 y 轴(见清单 21-33)。取而代之的是,变量 x1 将在以后仅被用作表示单个条的参考。
清单 21-33。ch21_04.html
...
var y = d3.scale.linear()
.range([h, 0]);
var xAxis = d3.svg.axis()
.scale(x0)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
...
在d3.csv()
函数中,用keys()
函数提取所有生产部门的名称,用filter()
函数从数组中过滤掉“国家”标题,如清单 21-34 所示。这里,您也为每个国家构建了一个对象数组,但是结构略有不同。新数组如下所示:
[Object { name="Electronics", value=12},
Object { name="Software", value=14},
Object { name="Mechanics", value=18}]
清单 21-34。ch21_04.html
...
d3.csv("data_05.csv", function(error, data) {
var sectorNames = d3.keys(data[0]).filter(function(key) {
return key !== "Country"; });
data.forEach(function(d) {
d.countries = sectorNames.map(function(name) {
return {name: name, value: +d[name]
};
});
...
});
一旦定义了数据结构,就可以定义新的域,如清单 21-35 所示。
清单 21-35。ch21_04.html
d3.csv("data_05.csv", function(error, data) {
...
data.forEach(function(d) {
...
});
x0.domain(data.map(function(d) { return d.Country; }));
x1.domain(sectorNames).rangeRoundBands([0, x0.rangeBand()]);
y.domain([0, d3.max(data, function(d) {
return d3.max(d.countries, function(d) { return d.value; });
})]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
...
如前所述,使用x0
您可以指定每个国家名称的有序域名。相反,在x1
中,各个扇区的名称组成了域名。最后,在y
中,域是由数值定义的。用新域更新迭代中传递的值(见清单 21-36)。
清单 21-36。ch21_04.html
d3.csv("data_05.csv", function(error, data) {
...
svg.append("g")
.attr("class", "grid")
.call(yGrid);
var country = svg.selectAll(".country")
.data(data)
.enter().append("g")
.attr("class", "country")
.attr("transform", function(d) {
return "translate(" + x0(d.Country) + ",0)";
});
country.selectAll("rect")
.data(function(d) { return d.countries; })
.enter().append("rect")
.attr("width", x1.rangeBand())
.attr("x", function(d) { return x1(d.name); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return h - y(d.value); })
.style("fill", function(d) { return color(d.name); });
});
然后,在csv()
函数的外部,你可以定义 SVG 元素,它将代表 y 轴上的轴标签,如清单 21-37 所示。它不需要在csv()
函数中定义,因为它独立于 CSV 文件中包含的数据。
清单 21-37。ch21_04.html
d3.csv("data_05.csv", function(error, data) {
...
});
var labels = svg.append("g")
.attr("class","labels")
labels.append("text")
.attr("transform", "rotate(–90)")
.attr("y", 5)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Income");
最后一件事。。。您需要给图表添加一个合适的标题,如清单 21-38 所示。
清单 21-38。ch21_04.html
labels.append("text")
...
.text("Income");
var title = svg.append("g")
.attr("class","title")
title.append("text")
.attr("x", (w / 2))
.attr("y", -30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("A grouped bar chart");
而图 21-9 就是结果。
图 21-9。
A grouped bar chart
在前面的例子中,使用规范化条形图,您看到了表示图例的另一种方法。您已经通过在最后一个条上放置一些报告系列名称的标签建立了这个图例(参见图 21-8 )。实际上你使用了点标签。这些标签可以包含任何文本,并直接连接到图表中的单个值。此时,引入点标签。您将把它们放在每个条形的顶部,显示该条形所表示的数值。这大大增加了每种图表的可读性。
正如您对任何其他图表组件所做的那样,定义了PointLabels
变量后,您可以使用它来分配应用于相应选择的函数链。此外,对于这种类型的组件,它具有针对单个数据的特定值,您可以利用 CSV 文件中包含的数据的迭代。您想要迭代的数据与您用于条形图的数据相同。因此,你将同样的迭代function(d)
作为参数传递给data()
函数(见清单 21-39)。为了在条形顶部绘制数据,您将为每个PointLabel
应用一个translate()
转换。
清单 21-39。ch21_04.html
d3.csv("data_05.csv", function(error, data) {
...
country.selectAll("rect")
...
.attr("height", function(d) { return h - y(d.value); })
.style("fill", function(d) { return color(d.name); });
var pointlabels = country.selectAll(".pointlabels")
.data(function(d) { return d.countries; })
.enter().append("g")
.attr("class", "pointlabels")
.attr("transform", function(d) {
return "translate(" + x1(d.name) + "," + y(d.value) + ")";
})
.append("text")
.attr("dy", "-0.3em")
.attr("x", x1.rangeBand()/2)
.attr("text-anchor", "middle")
.text(function(d) { return d.value; });
...
});
最后,除了向图表中添加一个以经典格式分组的图例之外,没有什么要做的了(见清单 21-40)。
清单 21-40。ch21_04.html
d3.csv("data_05.csv", function(error, data) {
...
pointlabels.append("text")
...
.text(function(d) { return d.value; });
var legend = svg.selectAll(".legend")
.data(color.domain().slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + i * 20 + ")";
});
legend.append("rect")
.attr("x", w - 18)
.attr("y", 4)
.attr("width", 10)
.attr("height", 10)
.style("fill", color);
legend.append("text")
.attr("x", w - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
});
图 21-10 显示了带有点标签和线段图例的新图表。
图 21-10。
A grouped bar chart reporting the values above each bar
带有负值的水平条形图
到目前为止你只使用了正值,但是如果你同时有正值和负值呢?你如何用条形图来表示它们呢?例如,这个包含正值和负值的值序列(见清单 21-41)。
清单 21-41。ch21_05.html
var data = [4, 3, 1, -7, -10, -7, 1, 5, 7, -3, -5, -12, -7, -11, 3, 7, 8, -1];
在分析要显示的数据之前,开始给图表添加页边空白,如清单 21-42 所示。
清单 21-42。ch21_05.html
var data = [4, 3, 1, -7, -10, -7, 1, 5, 7, -3, -5, -12, -7, -11, 3, 7, 8, -1];
var margin = {top: 30, right: 10, bottom: 10, left: 30},
w = 700 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
在这种特殊情况下,您将使用水平条,输入数组中的值将在 x 轴上表示,值 0 位于中间。为了实现这一点,首先需要找到绝对值的最大值(包括负值和正值)。然后在线性标度上创建 x 变量,而 y 变量被赋给一个包含数据在输入数组中放置顺序的序数标度(见清单 21-43)。
清单 21-43。ch21_05.html
...
var margin = {top: 30, right: 10, bottom: 10, left: 30},
w = 700 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var xMax = Math.max(-d3.min(data), d3.max(data));
var x = d3.scale.linear()
.domain([-xMax, xMax])
.range([0, w])
.nice();
var y = d3.scale.ordinal()
.domain(d3.range(data.length))
.rangeRoundBands([0, h], .2);
在清单 21-44 中,你将两个刻度分配给相应的 x 轴和 y 轴。这一次,x 轴将绘制在图表的上部,而 y 轴将向下(y 值向下增长)。
清单 21-44。ch21_05.html
var y = d3.scale.ordinal()
.domain(d3.range(data.length))
.rangeRoundBands([0, h], .2);
var xAxis = d3.svg.axis()
.scale(x)
.orient("top");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
此时,除了开始实现绘图区域之外,没有什么要做的了。创建根<svg>
元素,指定先前定义的边距。然后,定义 x 轴和 y 轴(见清单 21-45)。
清单 21-45。ch21_05.html
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate("+x(0)+",0)")
.call(yAxis);
最后,您需要为每个要表示的条形插入一个<rect>
元素,小心地将条形分成两个不同的组:负条形和正条形(见清单 21-46)。必须区分这两个类别,以便使用 CSS 样式类(例如,颜色)分别设置它们的属性。
清单 21-46。ch21_05.html
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate("+x(0)+",0)")
.call(yAxis);
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", function(d) {
return d < 0 ? "bar negative" : "bar positive";
})
.attr("x", function(d) { return x(Math.min(0, d)); })
.attr("y", function(d, i) { return y(i); })
.attr("width", function(d) { return Math.abs(x(d) - x(0)); })
.attr("height", y.rangeBand());
事实上,如果你用 Firebug 分析图 21-11 中的结构,你会发现这个迭代在同一个组中创建了两种不同类型的条形,可以通过类名“正条形”和“负条形”的特征来识别通过这两个不同的名称,您可以应用两种不同的 CSS 样式来区分具有负值和正值的条形。
图 21-11。
Firebug shows how it is possible to distinguish the positive from the negative bars, indicating the distinction in the class of each rect element
根据我们刚才所说的,你为正负条形设置了样式类属性,如清单 21-47 所示。
清单 21-47。ch21_05.html
<style>
.bar.positive {
fill: red;
stroke: darkred;
}
.bar.negative {
fill: lightblue;
stroke: blue;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
}
body {
font: 14px sans-serif;
}
</style>
最后,你会得到图 21-12 中的图表,红色条代表正值,蓝色条代表负值。
图 21-12。
A horizontal bar chart
摘要
在这一章中,你已经涵盖了几乎所有与条形图实现相关的基本方面,这种类型的图表是在本书的第一部分中使用 jqPlot 库开发的。在这里,您使用了 D3 库。因此,您看到了如何逐个元素地实现一个简单的条形图;然后,您转到堆叠条形图和分组条形图的各种情况,最后看一个最特殊的情况:描绘负值的水平条形图。
在下一章中,您将继续使用相同的方法:您将学习如何以类似于使用 jqPlot 时所用的方式实现饼状图,但是这一次您将使用 D3 库。
二十二、D3 饼图
Abstract
在前一章中,你已经看到了条形图是如何表示某一类数据的。您还看到,从相同的数据结构开始,根据您的意图,您可以选择一种类型的图表,而不是另一种,以便强调数据的特定方面。例如,在选择标准化堆积条形图时,您希望关注每个部门在其所在国家产生的收入百分比。
在前一章中,你已经看到了条形图是如何表示某一类数据的。您还看到,从相同的数据结构开始,根据您的意图,您可以选择一种类型的图表,而不是另一种,以便强调数据的特定方面。例如,在选择标准化堆积条形图时,您希望关注每个部门在其所在国家产生的收入百分比。
通常,用条形图表示的数据也可以用饼图表示。在这一章中,你将学习如何使用 D3 库创建这种类型的图表。假设这个库不像 jqPlot 那样提供已经实现的图形,而是要求用户使用基本的标量矢量图形(SVG)元素来构建它们,那么我们将从如何构建圆弧和扇形开始。事实上,就像条形图的矩形和折线图的线条一样,如果您要实现饼图(使用扇形)或圆环图(使用弧线),这些形状是非常重要的。在您实现了饼图的经典示例之后,我们将通过创建一些变体来进一步深化这个主题。在本章的第二部分,您将处理圆环图,管理从逗号分隔值(CSV)文件中读取的多个数据系列。
最后,我们将用一张图来结束这一章,这张图我们还没有处理过:极区图。这种类型的图表是饼图的进一步发展,其中的切片不再包含在一个圆圈中,而是具有不同的半径。有了极区图,信息将不再仅仅由切片所占据的角度来表示,而是由它的半径来表示。
基本饼图
为了更好地突出条形图和饼图之间的相似之处,在本例中,您将使用与创建基本条形图相同的 CSV 文件(参见第二十一章中的“绘制条形图”一节)。因此,在本节中,您的目的是使用相同的数据实现相应的饼图。为了做到这一点,在你开始“烘烤”馅饼和油炸圈饼之前,你必须首先获得正确形状的“烤盘”。D3 库还允许您表示弧形,如拱形和扇形,尽管实际上没有这样的 SVG 元素。事实上,您很快就会看到,由于 D3 的一些方法,它可以像处理其他真正的 SVG 元素(矩形、圆形、直线等)一样处理圆弧和扇形。).一旦你对这些元素的实现有了信心,你创建一个基本的饼图的工作就差不多完成了。在这一部分的第二部分,你将制作一些主题的变化,主要是形状边框和颜色。
绘制一个基本的饼图
将注意力再次转向包含在名为data_04.csv
的 CSV 文件中的数据(参见清单 22-1)。
清单 22-1。data_04.csv
country,income
France,14
Russia,22
Japan,13
South Korea,34
Argentina,28
现在,我们将演示这些数据如何很好地适应饼图表示。首先,在清单 22-2 中,定义了绘图区域和边距。
清单 22-2。ch22_01a.html
var margin = {top: 70, right: 20, bottom: 30, left: 40},
w = 500 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
即使对于饼图,您也需要使用一系列颜色来区分它们之间的切片。一般来说,通常使用category10()
函数来创建一个颜色域,这就是你到目前为止所做的。在这个例子中,您可以做同样的事情,但是这并不总是必需的。因此,我们利用这个例子来看看如何传递自定义颜色序列。通过定义你喜欢的颜色来创建一个定制的例子,一个接一个,如清单 22-3 所示。
清单 22-3。ch22_01a.html
var margin = {top: 70, right: 20, bottom: 30, left: 40},
w = 500 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var color = d3.scale.ordinal()
.
range(["#ffc87c", "#ffeba8", "#f3b080", "#916800", "#dda66b"]);
以前你用rect
元素构建了条,而现在你必须处理圆的截面。因此,你正在处理圆、角、拱、半径等。在 D3 里,有一整套工具可以让你处理这些类型的对象,让你处理饼状图更加容易。
为了表示饼图的切片(圆形扇区),D3 为您提供了一个函数:d3.svg.arc()
。这个函数实际上定义了拱门。术语“弧”是指由一个角度和两个圆界定的特定几何表面,一个圆具有较小的半径(内半径),另一个圆具有较大的半径(外半径)。扇形,即饼图的切片,只不过是一个内径等于 0 的圆弧(见图 22-1 )。
图 22-1。
By increasing the inner radius, it is possible to switch from a circle sector to an arc
首先,计算与绘图区域大小一致的半径。然后,根据这个范围,你划定了外半径和内半径,在这个例子中是 0(见清单 22-4)。
清单 22-4。ch22_01a.html
...
var color = d3.scale.ordinal()
.range(["#ffc87c", "#ffeba8", "#f3b080", "#916800", "#dda66b"]);
var radius = Math.min(w, h) / 2;
var arc = d3.svg.arc()
.outerRadius(radius)
.innerRadius(0);
D3 还提供了一个定义饼图的函数:d3.layout.pie()
函数。此函数构建了一个布局,允许您以非常简单的方式计算弧的开始和结束角度。使用这样的函数不是强制性的,但是饼图布局会自动将数据数组转换为对象数组。因此,定义一个对收入值有迭代函数的饼图,如清单 22-5 所示。
清单 22-5。ch22_01a.html
...
var arc = d3.svg.arc()
.outerRadius(radius)
.innerRadius(0);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.income; });
现在,如清单 22-6 所示,插入根元素<svg>
,分配正确的维度和适当的translate()
转换。
清单 22-6。ch22_01a.html
...
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.income; });
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" +(w / 2 + margin.left) +
"," + (h / 2 + margin.top) + ")");
接下来,为了读取 CSV 文件中的数据,您将一如既往地使用d3.csv()
函数。在这里,您也必须确保收入是用数值而不是字符串来解释的。然后,用forEach()
函数编写迭代,并在收入值旁边加上“+”号,如清单 22-7 所示。
清单 22-7。ch22_01a.html
...
.append("g")
.attr("transform", "translate(" +(w/2+margin.left)+
"," +(h/2+margin.top)+ ")");
d3.csv("data_04.csv", function(error, data) {
data.forEach(function(d) {
d.income = +d.income;
});
});
现在是时候添加一个<arc>
项了,但是这个元素并不作为 SVG 元素存在。事实上,这里使用的是一个描述弧线形状的<path>
元素。正是 D3 本身通过pie()
和arc()
函数构建了相应的路径。这使你免去了一项实在太复杂的工作。你只剩下定义这些元素的任务,就像它们是<arc>
元素一样(见清单 22-8)。
清单 22-8。ch22_01a.html
d3.csv("data_04.csv", function(error, data) {
data.forEach(function(d) {
d.income = +d.income;
});
var g = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d) { return color(d.data.country); });
});
如果你用 Firebug 分析 SVG 结构,你可以在图 22-2 中看到弧线路径是自动创建的,并且每个切片都有一个<g>
元素。
图 22-2。
With Firebug, you can see how the D3 library automatically builds the arc element
此外,有必要给每个切片添加一个指示性标签,以便您可以理解它与哪个国家相关,如清单 22-9 所示。注意arc.centroid()
功能。该函数计算圆弧的质心。质心被定义为内半径和外半径以及起始角度和终止角度之间的中点。因此,标签文本完美地出现在每个切片的中间。
清单 22-9。ch22_01a.html
d3.csv("data_04.csv", function(error, data) {
...
g.append("path")
.attr("d", arc)
.style("fill", function(d) { return color(d.data.country); });
g.append("text")
.attr("transform", function(d) {
return "translate(" + arc.centroid(d) + ")"; })
.style("text-anchor", "middle")
.text(function(d) { return d.data.country; });
});
即使对于饼状图,在顶部和中心位置添加标题也是一个好习惯(见清单 22-10)。
清单 22-10。ch22_01a.html
d3.csv("data_04.csv", function(error, data) {
...
g.append("text")
.attr("transform", function(d) {
return "translate(" + arc.centroid(d) + ")"; })
.style("text-anchor", "middle")
.text(function(d) { return d.data.country; });
var title = d3.select("svg").append("g")
.attr("transform", "translate(" +margin.left+ "," +margin.top+ ")")
.attr("class","title")
title.append("text")
.attr("x", (w / 2))
.attr("y", -30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("My first pie chart");
});
至于 CSS 类属性,您可以添加清单 22-11 中的定义。
清单 22-11。ch22_01a.html
<style>
body {
font: 16px sans-serif;
}
.arc path {
stroke: #000;
}
</style>
最后,你使用 D3 库得到了你的第一个饼状图,如图 22-3 所示。
图 22-3。
A simple pie chart
饼图的一些变化
现在,您将对刚刚创建的基本饼图进行一些更改,展示您可以获得的主题变化的无限可能性:
- 处理颜色序列;
- 对饼图中的切片进行排序;
- 在切片之间添加空间;
- 仅用轮廓表示切片;
- 结合以上所有内容。
处理颜色序列
在前面的例子中,我们定义了色阶中的颜色,在前面的例子中我们使用了category10()
函数。还有其他已经定义的分类色标:category20()
、category20b()
和category20c()
。将它们应用到您的饼图中,看看它们如何影响其外观。清单 22-12 显示了一个使用category10()
函数的例子。对于其他类别,您只需将此函数替换为其他函数。
清单 22-12。ch22_01b.html
var color = d3.scale.category10();
图 22-4 显示了刻度之间的颜色变化(反映在打印的不同灰度色调中)。category10()和 category20()函数生成具有交替颜色的刻度;相反,类别 20b()和类别 20c()会生成一个颜色渐变缓慢的刻度。
图 22-4。
Different color sequences: a) category10, b) category20, c) category20b, d) category20c
对饼图中的扇区进行排序
另一件要注意的事情是,默认情况下,D3 中的饼图经历了隐式排序。因此,如果您没有通过将null
传递给sort()
函数来显式地发出请求,如清单 22-13 所示。
清单 22-13。ch22_01c.html
var pie = d3.layout.pie()
//.sort(null)
.value(function(d) { return d.income; });
然后,饼图看起来会有所不同,如图 22-5 所示。
图 22-5。
A simple pie chart with sorted slices
在饼图中,第一个切片最大,然后其他切片按降序逐渐添加。
在切片之间添加空间
通常,切片显示为在它们之间间隔开,这可以非常容易地实现。您只需要对清单 22-14 所示的path
元素的 CSS 样式类进行一些修改。
清单 22-14。ch22_01d.html
.arc path {
stroke: #fff;
stroke-width: 4;
}
图 22-6 显示了当扇区被白色间隙隔开时,饼图如何呈现更令人愉悦的外观。
图 22-6。
The slices are separated by a white space
仅用轮廓表示切片
绘制带有切片的饼图要稍微复杂一些,这些切片只有彩色边框,内部是空的。您已经看到了 jqPlot 的类似案例。更改 CSS 样式类,如清单 22-15 所示。
清单 22-15。ch22_01e.html
.arc path {
fill: none;
stroke-width: 6;
}
事实上,这一次您不希望用特定的颜色填充切片,而是希望定义它们的边用特定的颜色着色。所以需要在 SVG 元素的样式定义中用stroke
属性替换fill
。现在是用指示色着色的线。但是你需要做另一个改变,这个改变有点复杂,很难理解。
您使用每个切片的边界来指定彩色部分,但它们实际上是重叠的。所以,下面的颜色覆盖了前一个颜色的一部分,把所有的切片都连在一起就不那么整齐了。再加个小缺口就更好了。这很容易做到,只需对每个切片进行平移。每个切片都应该在离心方向上偏离中心一小段距离。因此,每个切片的平移是不同的,这里你利用了centroid()
函数的功能,它给出了平移的方向(x 和 y 坐标)(见清单 22-16)。
清单 22-16。ch22_01e.html
var g = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc")
.attr("transform", function(d) {
a = arc.centroid(d)[0]/6;
b = arc.centroid(d)[1]/6;
return "translate(" + a +","+b + ")";
})
g.append("path")
.attr("d", arc)
.style("stroke", function(d) { return color(d.data.country); });
图 22-7 展示了这些变化如何影响饼图。
图 22-7。
A pie chart with unfilled slices
混合所有这些
但这并没有结束。您可以在这最后两个饼图之间创建一个中间解决方案:获取具有更深颜色边缘的切片,并用更浅的颜色填充它们。定义两条相同但颜色不同的路径就足够了,如清单 22-17 所示。第一种颜色均匀,略显暗淡,而第二种只有彩色边缘,内部为白色。
清单 22-17。ch22_01f.html
var g = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc")
.attr("transform", function(d) {
a = arc.centroid(d)[0]/6;
b = arc.centroid(d)[1]/6;
return "translate(" + a +","+b + ")";
})
g.append("path")
.attr("d", arc)
.style("fill", function(d) { return color(d.data.country); })
.attr('opacity', 0.5);
g.append("path")
.attr("d", arc)
.style("stroke", function(d) { return color(d.data.country); });
图 22-8 显示了带有两条路径的间隔饼图,这两条路径为切片及其边界着色。
图 22-8。
A different way to color the slices in a pie chart
圆环图
就像饼图对于条形图一样,圆环图对于多系列条形图也是如此。事实上,当您有多组值时,您必须用每个系列的饼图来表示它们。如果你使用甜甜圈图,你可以把它们放在一起,并在一个图表中进行比较(见图 22-9 )。
图 22-9。
A diagram representing the parallelism between pie charts and bar charts both with one and with multiple series of data
首先编写清单 22-18 中的代码;我们将不提供任何解释,因为它与前面的例子相同。
清单 22-18。ch22_02.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="
http://d3js.org/d3.v3.js
<style>
body {
font: 16px sans-serif;
}
.arc path {
stroke: #000;
}
</style>
</head>
<body>
<script type="text/javascript">
var margin = {top: 70, right: 20, bottom: 30, left: 40},
w = 500 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var color = d3.scale.ordinal()
.range(["#ffc87c", "#ffeba8", "#f3b080", "#916800", "#dda66b"]);
var radius = Math.min(w, h) / 2;
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" +(w/2+margin.left)+
"," +(h/2+margin.top)+ ")");
var title = d3.select("svg").append("g")
.attr("transform", "translate(" +margin.left+ "," +margin.top+ ")")
.attr("class","title");
title.append("text")
.attr("x", (w / 2))
.attr("y", -30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("A donut chart");
</script>
</body>
</html>
对于一个多系列数据的例子,您将向文件data_04.csv
中添加另一列表示费用的数据,如清单 22-19 所示,并且您将把这个新版本保存为data_06.csv
。
清单 22-19。data_06.csv
country,income,expense
France,14,10
Russia,22,19
Japan,13,6
South Korea,34,12
Argentina,28,26
您添加了一组新数据。因此,与上一个示例不同,您必须为此系列创建一个新弧线。然后,除了第二条弧线之外,再添加第三条弧线。这个弧不会绘制一个系列的切片,但是您将使用它来循环分布标签。这些标签显示了国家的名称,为图例提供了另一种选择。因此,将半径分成三部分,中间留一个空隙来分隔系列,如清单 22-20 所示。
清单 22-20。ch22_02.html
var arc1 = d3.svg.arc()
.outerRadius(0.4 * radius)
.innerRadius(0.2 * radius);
var arc2 = d3.svg.arc()
.outerRadius(0.7 * radius )
.innerRadius(0.5 * radius );
var arc3 = d3.svg.arc()
.outerRadius(radius)
.innerRadius(0.8 * radius);
您刚刚创建了两个弧来管理这两个系列,因此现在有必要创建两个饼图,一个用于收入值,另一个用于支出值(见清单 22-21)。
清单 22-21。ch22_02.html
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.income; });
var pie2 = d3.layout.pie()
.sort(null)
.value(function(d) { return d.expense; });
使用d3.csv()
函数读取文件中的数据,如清单 22-22 所示。您使用forEach()
进行通常的数据迭代,将收入和费用解释为数值。
清单 22-22。ch22_02.html
d3.csv("data_06.csv", function(data) {
data.forEach(function(d) {
d.income = +d.income;
d.expense = +d.expense;
});
});
在清单 22-23 中,您创建了path
元素,它绘制了两个甜甜圈的不同部分,对应于两个系列。使用函数data()
,您将两个饼图布局的数据绑定到两个表示。两个甜甜圈必须遵循相同的颜色顺序。一旦定义了 path 元素,就可以用一个报告相应数值的text
元素来连接它。因此,您添加了一些标签,使图表更容易阅读。
清单 22-23。ch22_02.html
var g = svg.selectAll(".arc1")
.data(pie(data))
.enter().append("g")
.attr("class", "arc1");
g.append("path")
.attr("d", arc1)
.style("fill", function(d) { return color(d.data.country); });
g.append("text")
.attr("transform", function(d) {
return "translate(" + arc1.centroid(d) + ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.income; });
var g = svg.selectAll(".arc2")
.data(pie2(data))
.enter().append("g")
.attr("class", "arc2");
g.append("path")
.attr("d", arc2)
.style("fill", function(d) { return color(d.data.country); });
g.append("text")
.attr("transform", function(d) {
return "translate(" + arc2.centroid(d) + ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.expense; });
现在,剩下要做的就是添加执行图例功能的外部标签,如清单 22-24 所示。
清单 22-24。ch22_02.html
g.append("text")
.attr("transform", function(d) {
return "translate(" + arc3.centroid(d) + ")"; })
.style("text-anchor", "middle")
.text(function(d) { return d.data.country; });
这样你就得到了如图 22-10 所示的环形图。
图 22-10。
A donut chart
极区图
极区图非常类似于饼图,但它们的不同之处在于每个扇区从圆心延伸的距离,从而可以表示更远的值。每个切片的范围与这个新的附加值成比例(见图 22-11 )。
图 22-11。
In a polar area diagram, each slice is characterized by a radius r and an angle
再次考虑文件data_04.csv
中的数据,添加一个额外的列来显示相应国家的增长,如清单 22-25 所示。另存为data_07.csv
。
清单 22-25。data_07.csvl
country,income,growth
France,14,10
Russia,22,19
Japan,13,9
South Korea,34,12
Argentina,28,16
开始编写清单 22-26 中的代码;同样,我们不会解释这一部分,因为它与前面的例子相同。
清单 22-26。ch22_03.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="
http://d3js.org/d3.v3.js
<style>
body {
font: 16px sans-serif;
}
.arc path {
stroke: #000;
}
</style>
</head>
<body>
<script type="text/javascript">
var margin = {top: 70, right: 20, bottom: 30, left: 40},
w = 500 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var color = d3.scale.ordinal()
.range(["#ffc87c", "#ffeba8", "#f3b080", "#916800", "#dda66b"]);
var radius = Math.min(w, h) / 2;
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.income; });
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" +(w/2-margin.left)+
"," +(h/2+margin.top)+ ")");
var title = d3.select("svg").append("g")
.attr("transform", "translate(" +margin.left+ "," +margin.top+ ")")
.attr("class","title");
title.append("text")
.attr("x", (w / 2))
.attr("y", -30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("A polar area diagram");
</script>
</body>
</html>
在清单 22-27 中,您用d3.csv()
函数读取了data_07.csv
文件中的数据,并确保收入和增长率的值被解释为数值。
清单 22-27。ch22_03.html
d3.csv("data_07.csv", function(error, data) {
data.forEach(function(d) {
d.income = +d.income;
d.growth = +d.growth;
});
});
与前面的例子不同,这里你不仅定义了一个弧线,而且定义了一个随着被读取数据的变化而变化的弧线;我们称之为arcs
,因为outerRadius
不再是常数,而是与文件中的增长值成比例。为了做到这一点,你需要应用一个通用的迭代函数,然后弧线必须在d3.csv()
函数中声明(见清单 22-28)。
清单 22-28。ch22_03.html
d3.csv("data_07.csv", function(error, data) {
data.forEach(function(d) {
d.income = +d.income;
d.growth = +d.growth;
});
arcs = d3.svg.arc()
.innerRadius( 0 )
.outerRadius( function(d,i) { return 8*d.data.growth; });
});
现在,您只需添加 SVG 元素,这些元素绘制带有包含增长和收入值的标签的切片(参见清单 22-29)。报告收入值的标签将绘制在切片内,就在由centroid()
函数返回的值处。相反,关于报告生长值的标签,它们将被画在切片的外面。要获得这种效果,您可以使用由centroid()
返回的 x 和 y 值,并将它们乘以一个大于 2 的值。你一定记得质心在角度的正中心,在innerRadius
和outerRadius
的中间。因此,将它们乘以 2,就得到切片外边缘中心的点。如果您将它们乘以一个大于 2 的值,那么您将找到切片外部的 x 和 y 位置,就在您想要绘制具有增长值的标签的位置。
清单 22-29。ch22_03.html
var g = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arcs)
.style("fill", function(d) { return color(d.data.country); });
g.append("text")
.attr("class","growth")
.attr("transform", function(d) {
a = arcs.centroid(d)[0]*2.2;
b = arcs.centroid(d)[1]*2.2;
return "translate(" +a+","+b+ ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.growth; });
g.append("text")
.attr("class","income")
.attr("transform", function(d) {
return "translate(" +arcs.centroid(d)+ ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.income; });
您尚未对饼图执行的一项操作是添加图例。在清单 22-30 中,我们在d3.csv()
函数之外定义了一个元素<g>
来插入图例表,在函数内部我们定义了所有与国家相关的元素,因为定义它们需要访问文件中的值。
清单 22-30。ch22_03.html
var legendTable = d3.select("svg").append("g")
.attr("transform", "translate(" +margin.left+ ","+margin.top+")")
.attr("class","legendTable");
d3.csv("data_07.csv", function(error, data) {
...
var legend = legendTable.selectAll(".legend")
.data(pie(data))
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", w - 18)
.attr("y", 4)
.attr("width", 10)
.attr("height", 10)
.style("fill", function(d) { return color(d.data.country); });
legend.append("text")
.attr("x", w - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d.data.country; });
});
最后,您可以对 CSS 样式类进行一些调整,如清单 22-31 所示。
清单 22-31。ch22_03.html
<style>
body {
font: 16px sans-serif;
}
.arc path {
stroke: #fff;
stroke-width: 4;
}
.arc .income {
font: 12px Arial;
color: #fff;
}
</style>
这里是极区图(见图 22-12 )。
图 22-12。
A polar area diagram
摘要
在这一章中,你学习了如何使用 D3 库实现饼状图和圆环图,遵循了与前几章几乎相同的指导方针。此外,在本章的最后,你学习了如何制作极区图,这是一种你以前没有见过的图表,D3 库允许你很容易地实现。
在下一章中,您将实现两种类型的烛台图表,您已经在介绍 jqPlot 库的书的第一部分中讨论过了,只是这次您将使用 D3 库。
二十三、D3 蜡烛图
Abstract
在这简短但重要的一章中,你会看到蜡烛图。这种类型的图表是基于一种特殊的数据格式(OHLC,或开盘-盘高-盘低-收盘),你已经在介绍 jqPlot 库时处理过了(见第十二章)。使用 jqPlot,您有一个特殊的插件来以适当的方式处理和表示这样的数据;相反,用 D3 你必须一个接一个地构建所有的图形元素,最重要的是你需要实现一个解析器从外部文件读取 OHLC 数据。此外,您需要解决的另一个重要方面是如何处理日期和时间数据。
在这简短但重要的一章中,你会看到蜡烛图。这种类型的图表是基于一种特殊的数据格式(OHLC,或开盘-盘高-盘低-收盘),你已经在 jqPlot 库介绍时处理过了(见第十二章)。使用 jqPlot,您有一个特殊的插件来以适当的方式处理和表示这样的数据;相反,用 D3 你必须一个接一个地构建所有的图形元素,最重要的是你需要实现一个解析器从外部文件读取 OHLC 数据。此外,您需要解决的另一个重要方面是如何处理日期和时间数据。
虽然这听起来很复杂,但在这一章中,你会发现 D3 库是如何为你提供工具,让事情变得简单而直接。
您将首先从构建一个简单的 OHLC 图表开始,以便特别关注 OHLC 数据的读取。然后您将详细了解 D3 如何处理日期和时间数据,最后您将仅使用标量矢量图形(SVG)元素(如线条)来表示 OHLC 图表。
在最后一部分,你将通过一些修改把你的 OHLC 图表转换成一个更完整的烛台图表。
创建 OHLC 图表
因为 D3 能够从小的图形组件构建新的图形结构,所以您还可以创建像用 jqPlot 生成的那些蜡烛图。您已经看到蜡烛图需要定义良好的数据结构:由日期和四个 OHLC 值组成的数据时间轴。您将清单 23-1 中的数据复制到一个文件中,并保存为data_08.csv
。
清单 23-1。data_08.csv
date,open,min,max,close,
08/08/2012,1.238485,1.2327,1.240245,1.2372,
08/09/2012,1.23721,1.22671,1.23873,1.229295,
08/10/2012,1.2293,1.22417,1.23168,1.228975,
08/12/2012,1.229075,1.22747,1.22921,1.22747,
08/13/2012,1.227505,1.22608,1.23737,1.23262,
08/14/2012,1.23262,1.23167,1.238555,1.232385,
08/15/2012,1.232385,1.22641,1.234355,1.228865,
08/16/2012,1.22887,1.225625,1.237305,1.23573,
08/17/2012,1.23574,1.22891,1.23824,1.2333,
08/19/2012,1.23522,1.23291,1.235275,1.23323,
08/20/2012,1.233215,1.22954,1.236885,1.2351,
08/21/2012,1.23513,1.23465,1.248785,1.247655,
08/22/2012,1.247655,1.24315,1.254415,1.25338,
08/23/2012,1.25339,1.252465,1.258965,1.255995,
08/24/2012,1.255995,1.248175,1.256665,1.2512,
08/26/2012,1.25133,1.25042,1.252415,1.25054,
08/27/2012,1.25058,1.249025,1.25356,1.25012,
08/28/2012,1.250115,1.24656,1.257695,1.2571,
08/29/2012,1.25709,1.251895,1.25736,1.253065,
08/30/2012,1.253075,1.248785,1.25639,1.25097,
08/31/2012,1.25096,1.249375,1.263785,1.25795,
09/02/2012,1.257195,1.256845,1.258705,1.257355,
09/03/2012,1.25734,1.25604,1.261095,1.258635,
09/04/2012,1.25865,1.25264,1.262795,1.25339,
09/05/2012,1.2534,1.250195,1.26245,1.26005,
09/06/2012,1.26006,1.256165,1.26513,1.26309,
09/07/2012,1.26309,1.262655,1.281765,1.281625,
09/09/2012,1.28096,1.27915,1.281295,1.279565,
09/10/2012,1.27957,1.27552,1.28036,1.27617,
09/11/2012,1.27617,1.2759,1.28712,1.28515,
09/12/2012,1.28516,1.281625,1.29368,1.290235,
现在这几乎已经成为一种习惯,你从编写几乎所有图表通用的代码开始,不需要更多的解释(见清单 23-2)。
清单 23-2。ch23_01.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="
http://d3js.org/d3.v3.js
<style>
body {
font: 16px sans-serif;
}
</style>
</head>
<body>
<script type="text/javascript">
var margin = {top: 70, right: 20, bottom: 30, left: 40},
w = 500 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" +margin.left+ "," +margin.top+ ")");
var title = d3.select("svg").append("g")
.attr("transform", "translate(" +margin.left+ "," +margin.top+ ")")
.attr("class","title");
title.append("text")
.attr("x", (w / 2))
.attr("y", -30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("My candlestick chart");
</script>
</body>
</html>
因为在文件的第一列有日期类型的值,你需要定义一个解析器来设置它们的格式(见清单 23-3)。
清单 23-3。ch23_01.html
...
w = 500 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var parseDate = d3.time.format("%m/%d/%Y").parse;
...
烛台图表是一种通常是时间性的数据表示,即,四个 OHLC 数据与单个时间单位相关,并且它们随时间的变化沿着 x 轴是可见的。因此,您将有一个 x 轴,您必须在其上处理时间值,而在 y 轴上,您将分配一个线性刻度。在定义 x 轴时,你要确保报告的日期只显示日和月,这将由前三个字符表示(见清单 23-4)。
清单 23-4。ch23_01.html
var parseDate = d3.time.format("%m/%d/%Y").parse;
var x = d3.time.scale()
.range([0, w]);
var y = d3.scale.linear()
.range([h, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickFormat(d3.time.format("%d-%b"))
.ticks(5);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
...
现在观察数据文件中的内容(清单 23-1),你可以看到五列数据,其中最后四列是数字。第一列包含必须提交给解析器的日期,而其他四列被解释为数值。此外,您需要找出所有 OHLC 数据中的最大值和最小值。在迭代函数forEach()
中管理所有这些方面,如清单 23-5 所示。
清单 23-5。ch23_01.html
...
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" +margin.left+ "," +margin.top+ ")");
d3.csv("data_08.csv", function(error, data) {
var maxVal = -1000;
var minVal = 1000;
data.forEach(function(d) {
d.date = parseDate(d.date);
d.open = +d.open;
d.close = +d.close;
d.max = +d.max;
d.min = +d.min;
if (d.max > maxVal)
maxVal = d.max;
if (d.min < minVal)
minVal = d.min;
});
});
...
接下来,在清单 23-6 中,创建 x 和 y 的域。在 x 轴上,域将处理日期,y 域将有一个扩展,它将覆盖刚刚找到的最小值和最大值之间的所有值(minVal
和maxVal
)。
清单 23-6。ch23_01.html
d3.csv("data_08.csv", function(error, data) {
data.forEach(function(d) {
...
});
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([minVal,maxVal]);
});
一旦定义好了域,你就可以用 SVG 元素和它们的标签画出 x 和 y 两个轴,如清单 23-7 所示。
清单 23-7。ch23_01.html
d3.csv("data_08.csv", function(error, data) {
...
y.domain([minVal,maxVal]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis)
svg.append("text")
.attr("class", "label")
.attr("x", w)
.attr("y", -6)
.style("text-anchor", "end");
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Dollar [$]");
});
使用 SVG 元素<line>
在 OHLC 图上绘制数据(见清单 23-8)。ext
线是定义高值和低值之间范围的垂直线。close
和open
线是两条水平线,分别对应于打开和关闭值。
清单 23-8。ch23_01.html
d3.csv("data_08.csv", function(error, data) {
...
svg.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Dollar [$]")
svg.selectAll("line.ext")
.data(data)
.enter().append("svg:line")
.attr("class", "ext")
.attr("x1", function(d) { return x(d.date)})
.attr("x2", function(d) { return x(d.date)})
.attr("y1", function(d) { return y(d.min);})
.attr("y2", function(d) { return y(d.max); });
svg.selectAll("line.close")
.data(data)
.enter().append("svg:line")
.attr("class", "close")
.attr("x1", function(d) { return x(d.date)+5})
.attr("x2", function(d) { return x(d.date)-1})
.attr("y1", function(d) { return y(d.close);})
.attr("y2", function(d) { return y(d.close); });
svg.selectAll("line.open")
.data(data)
.enter().append("svg:line")
.attr("class", "open")
.attr("x1", function(d) { return x(d.date)+1})
.attr("x2", function(d) { return x(d.date)-5})
.attr("y1", function(d) { return y(d.open);})
.attr("y2", function(d) { return y(d.open); });
});
感谢您定义新生成元素的类的方式,您可以通过使用line
类,或者使用line.open
、line.close
和line.ext
类分别定义它们,为所有三行定义 CSS 样式的属性(参见清单 23-9)。
清单 23-9。ch23_01.html
<style>
body {
font: 16px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
line.open, line.close, line.ext {
stroke: blue;
stroke-width: 2;
shape-rendering: crispEdges;
}
</style>
最后你得到的是图 23-1 所示的烛台图,和用 jqPlot 得到的那些没什么好羡慕的。
图 23-1。
An OHLC chart
日期格式
在处理这类利用 OHLC 数据的图表时,您总是要处理 x 轴上的时间和日期值。因此,根据这个观察,分析 D3 库如何处理这种类型的数据。
如果在前面的示例中没有用零填充日期和月份,或者报告的年份只有两位数(例如,“8/9/12”),会发生什么情况?在d3.csv()
函数中,D3 无法读取这种格式的日期,因此,烛台图表就不会出现。实际上,您需要做的事情非常简单,即猜测要插入解析器的格式化程序的正确序列。对于格式化程序,我们指的是前面带有“%”符号的一组字符,它根据特定的(区分大小写)字符来表示以某种方式书写的时间单位。
var parseDate = d3.time.format("%m/%e/%y").parse;
甚至字面上表示的日期也可以用同样的方式处理。您已经见过这种日期格式:
08-Aug-12,1.238485,1.2327,1.240245,1.2372,
它可以用这个解析器来处理:
var parseDate = d3.time.format("%d-%b-%y").parse;
但是还有更复杂的情况,例如:
Monday 16 April 2012,1.238485,1.2327,1.240245,1.2372,
它可以用这个解析器来处理:
var parseDate = d3.time.format("%A %e %B %Y").parse;
不同值之间的所有分隔字符(包括空格)应该在解析器中的相同位置报告。因此,如果日期是这样定义的。。。
'8 Aug-12',1.238485,1.2327,1.240245,1.2372,
您必须在定义解析器的字符串中插入空格和引号,否则日期将无法识别。
var parseDate = d3.time.format("'%d %b-%y'").parse;
您还必须记住,csv 文件中唯一不能添加的分隔字符是“,”。如果您必须插入它,您必须使用 TSV(制表符分隔值)文件。
表 23-1 包括所有可用的格式化程序。它们的组合应该覆盖任何输入大小。
表 23-1。
D3 Date and Time Formatters
| 格式程序 | 描述 | | --- | --- | | %a | 缩写的工作日名称 | | %A | 完整的工作日名称 | | %b | 缩写月份名 | | %B | 完整的月份名称 | | %c | 日期和时间,格式为“%a %b %e %H:%M:%S %Y” | | %d | 以十进制数字[01,31]形式用零填充的一个月中的某一天 | | %e | 以十进制数字[ 1,31]表示的一个月中的第几天 | | %H | 以十进制数[00,23]表示的小时(24 小时制) | | %I | 以十进制数[01,12]表示的小时(12 小时制) | | %j | 以十进制数表示的一年中的某一天[001,366] | | %m | 十进制数字形式的月份[01,12] | | %M | 十进制数形式的分钟[00,59] | | %p | 上午或下午 | | %S | 十进制数形式的秒[00,61] | | %U | 以十进制数[00,53]表示的一年中的周数(星期日是一周的第一天) | | %w | 以十进制数表示的工作日[0(星期日),6] | | %W | 以十进制数[00,53]表示的一年中的周数(星期一是一周的第一天) | | %x | 日期,作为“%m/%d/%y” | | %X | 时间,作为“%H:%M:%S” | | %y | 没有世纪作为十进制数的年份[00,99] | | %Y | 以世纪为小数的年份 | | %Z | 时区偏移量,例如“-0700” | | %% | 文字“%”字符 |蜡烛图中的方框表示
使用 jqPlot,您还看到了显示 OHLC 数据的其他方法。例如,这种数据通常由一条垂直线和一个覆盖它一定长度的垂直方框来表示。垂直线与前面的烛台相同,它位于 OHLC 的高值和低值之间。相反,该框表示开盘价和收盘价之间的范围。此外,如果开盘价大于收盘价,盒子将是给定的颜色,但如果相反,将是另一种颜色。
您使用了包含在data_08.csv
文件中的相同数据,并且从上一个示例中的代码开始,您将看到将要进行的更改。
用这三条新的线替换ext
、open
和close
:ext
、ext1
和ext2
(见清单 23-10)。然后你必须添加代表盒子的矩形。线应该是黑色的,而当开盘价大于收盘价时,方框应该是红色的,否则,在相反的情况下,方框将是绿色的。
清单 23-10。ch23_02.html
svg.selectAll("line.ext")
.data(data)
.enter().append("svg:line")
.attr("class", "ext")
.attr("x1", function(d) { return x(d.date)})
.attr("x2", function(d) { return x(d.date)})
.attr("y1", function(d) { return y(d.min);})
.attr("y2", function(d) { return y(d.max);});
svg.selectAll("line.ext1")
.data(data)
.enter().append("svg:line")
.attr("class", "ext")
.attr("x1", function(d) { return x(d.date)+3})
.attr("x2", function(d) { return x(d.date)-3})
.attr("y1", function(d) { return y(d.min);})
.attr("y2", function(d) { return y(d.min); });
svg.selectAll("line.ext2")
.data(data)
.enter().append("svg:line")
.attr("class", "ext")
.attr("x1", function(d) { return x(d.date)+3})
.attr("x2", function(d) { return x(d.date)-3})
.attr("y1", function(d) { return y(d.max);})
.attr("y2", function(d) { return y(d.max); });
svg.selectAll("rect")
.data(data)
.enter().append("svg:rect")
.attr("x", function(d) { return x(d.date)-3; })
.attr("y", function(d) { return y(Math.max(d.open, d.close));})
.attr("height", function(d) {
return y(Math.min(d.open, d.close))-y(Math.max(d.open, d.close));})
.attr("width",6)
.attr("fill",function(d) {
return d.open > d.close ? "darkred" : "darkgreen" ;});
});
最后一件事是在清单 23-11 中设置 CSS 样式类。
清单 23-11。ch23_02.html
<style>
body {
font: 16px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
line.ext, line.ext1, line.ext2 {
stroke: #000;
stroke-width: 1;
shape-rendering: crispEdges;
}
</style>
而图 23-2 中的图表就是结果。
图 23-2。
A candlestick chart
摘要
在本章中,您已经看到了在 jqPlot 库的第一部分中已经讨论过的烛台图表的类型,但是这次您使用了 D3。您已经看到了如何轻松获得类似的结果,同时保持对每个图形元素的完全控制。此外,由于这种图表使用时间数据,这里您已经深入研究了 D3 库如何管理这种类型的数据以及管理格式的各种方法。
继续关注 jqPlot 库和 D3 库在实现各种类型的图表方面的并行性,在下一章你将学习散点图和气泡图,以及如何用 D3 库实现它们。
二十四、D3 散点图和气泡图
Abstract
在本章中,您将了解散点图。每当您有一组数据对[x,y]并且想要分析它们在 xy 平面上的分布时,您都会参考这种类型的图表。因此,您将首先看到如何使用 D3 库制作这种类型的图表。在第一个示例中,您将开始读取包含多个数据系列的 TSV(制表符分隔值)文件,通过它们,您将看到如何实现散点图。
在本章中,您将了解散点图。每当您有一组数据对[x,y]并且想要分析它们在 xy 平面上的分布时,您都会参考这种类型的图表。因此,您将首先看到如何使用 D3 库制作这种类型的图表。在第一个示例中,您将开始读取包含多个数据系列的 TSV(制表符分隔值)文件,通过它们,您将看到如何实现散点图。
完成散点图后,您将看到如何使用具有特定形状的标记来表示数据点,无论是从预定义的集合中选择还是通过创建原始标记。
这类图表非常重要。它是分析数据分布的基本工具;事实上,从这些图表中,您可以找到特定的趋势(趋势线)和分组(聚类)。在这一章中,两个简单的例子将向你展示如何表示趋势线和聚类。
此外,您将看到如何通过事件处理将突出显示功能添加到图表中,以及 D3 库如何管理它。
最后,本章将以最后一个例子结束,在这个例子中,你需要用三个参数[x,y,z]来表示数据。因此,适当地修改散点图,你会发现你可以得到一个气泡图,这是一个散点图修改,以能够代表一个额外的参数。
散点图
由于有了 D3 库,您可以生成的图形表示没有限制,可以像组合砖块一样组合图形元素。散点图的创建也不例外。
你开始收集数据(见清单 24-1),这一次是以表格的形式(因此是一个 TSV 文件),你将复制并保存为一个名为data_09.tsv
的文件。(参见以下注释。)
Note
注意,TSV 文件中的值是用制表符分隔的,所以当你编写或复制清单 24-1 时,记得检查每个值之间只有一个制表符。
清单 24-1。data_09.tsv
time intensity group
10 171.11 Exp1
14 180.31 Exp1
17 178.32 Exp1
42 173.22 Exp3
30 145.22 Exp2
30 155.68 Exp3
23 200.56 Exp2
15 192.33 Exp1
24 173.22 Exp2
20 203.78 Exp2
18 187.88 Exp1
45 180.00 Exp3
27 181.33 Exp2
16 198.03 Exp1
47 179.11 Exp3
27 175.33 Exp2
28 162.55 Exp2
24 208.97 Exp1
23 200.47 Exp1
43 165.08 Exp3
27 168.77 Exp2
23 193.55 Exp2
19 188.04 Exp1
40 170.36 Exp3
21 184.98 Exp2
15 197.33 Exp1
50 188.45 Exp3
23 207.33 Exp1
28 158.60 Exp2
29 151.31 Exp2
26 172.01 Exp2
23 191.33 Exp1
25 226.11 Exp1
60 198.33 Exp3
假设文件中包含的数据属于三个不同的实验(标记为 Exp1、Exp2 和 Exp3),每个实验应用于一个不同的对象(例如,三种发光物质),您希望在其中测量它们的发射强度如何随时间变化。读数在不同的时间重复进行。您的目标是在 xy 平面上表示这些值,以便分析它们的分布和最终属性。
观察数据,您可以看到它们由三列组成:时间、强度和组成员。这是一种典型的数据结构,可以以散点图的形式显示。您将把时间刻度放在 x 轴上,把强度值放在 y 轴上,最后通过标记的形状或颜色来识别组,这些标记将标记散点图中点的位置。
按照惯例,您可以从编写清单 24-2 中的代码开始。这段代码代表您的起始代码,因为它对于您在前面的示例中看到的几乎所有图表都是通用的,所以不需要进一步解释。
清单 24-2。ch24_01.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="
http://d3js.org/d3.v3.js
<style>
body {
font: 16px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<script type="text/javascript">
var margin = {top: 70, right: 20, bottom: 40, left: 40},
w = 500 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var color = d3.scale.category10();
var x = d3.scale.linear()
.range([0, w]);
var y = d3.scale.linear()
.range([h, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left+ "," +margin.top+ ")");
var title = d3.select("svg").append("g")
.attr("transform", "translate(" + margin.left+ "," +margin.top+ ")")
.attr("class","title");
title.append("text")
.attr("x", (w / 2))
.attr("y", –30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("My Scatterplot");
</script>
</body>
</html>
在清单 24-3 中,你用d3.tsv()
函数从 TSV 文件中读取列表数据,确保数字值被读取。在这里,即使在第一列有时间,也不需要解析,因为它们是秒,因此可以被认为是线性的。
清单 24-3。ch24_01.html
...
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left+ "," +margin.top+ ")");
d3.tsv("data_09.tsv", function(error, data) {
data.forEach(function(d) {
d.time = +d.time;
d.intensity = +d.intensity;
});
});
var title = d3.select("svg").append("g")
.attr("transform", "translate(" + margin.left+ "," +margin.top+ ")")
.attr("class","title");
...
同样关于域,分配非常简单,如清单 24-4 所示。此外,您将使用nice()
函数,该函数对域的值进行舍入。
清单 24-4。ch24_01.html
d3.tsv("data_09.tsv", function(error, data) {
data.forEach(function(d) {
d.time = +d.time;
d.intensity = +d.intensity;
});
x.domain(d3.extent(data, function(d) { return d.time; })).nice();
y.domain(d3.extent(data, function(d) { return d.intensity; })).nice();
});
您还添加了轴标签,在 x 轴上显示“时间”,在 y 轴上显示“强度”,如清单 24-5 所示。
清单 24-5。ch24_01.html
d3.tsv("data_09.tsv", function(error, data) {
...
x.domain(d3.extent(data, function(d) { return d.time; })).nice();
y.domain(d3.extent(data, function(d) { return d.intensity; })).nice();
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
svg.append("text")
.attr("class", "label")
.attr("x", w)
.attr("y", h + margin.bottom - 5)
.style("text-anchor", "end")
.text("Time [s]");
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("text")
.attr("class", "label")
.attr("transform", "rotate(–90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Intensity");
});
最后,你必须直接在图上画出标记。这些可以用 SVG 元素<circle>
来表示。因此,散点图上显示的数据点将是半径为 3.5 像素的小点(见清单 24-6)。为了定义不同组的表示,标记以不同的颜色绘制。
清单 24-6。ch24_01.html
d3.tsv("data_09.tsv", function(error, data) {
...
svg.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Intensity");
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.intensity); })
.style("fill", function(d) { return color(d.group); });
});
现在你在散点图上有这么多彩色标记,但是没有参考它们的颜色和它们所属的组。因此,有必要添加一个图例,显示与不同颜色相关的各个组的名称(见清单 24-7)。
清单 24-7。ch24_01.html
d3.tsv("data_09.tsv", function(error, data) {
...
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.intensity); })
.style("fill", function(d) { return color(d.group); });
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + (i * 20) + ")"; });
legend.append("rect")
.attr("x", w - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", w - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
});
所有工作完成后,你得到如图 24-1 所示的散点图。
图 24-1。
A scatterplot showing the data distribution
标记和符号
当您想要表示散点图时,不可低估的一个方面是您想要用来表示数据点的标记的形状。毫不奇怪,D3 库为您提供了许多通过符号管理标记表示的方法。在本章中,您将了解这个主题,因为它非常适合这种图表(散点图),但不会改变它对其他类型图表(如折线图)的应用。
使用符号作为标记
D3 库提供了一组可以直接用作标记的符号。在表 24-1 中,你可以看到一个报告各种预定义符号的列表。
表 24-1。
Predefined Symbols in D3 Library
| 标志 | 描述 | | --- | --- | | 圆 | 一个圆圈 | | 十字架 | 希腊十字(或加号) | | 钻石 | 菱形 | | 平方 | 轴对齐的正方形 | | 三角形向下 | 向下的等边三角形 | | 三角形向上 | 向上的等边三角形 |继续前面的示例,您将散点图中的点替换为用作标记的不同符号。这些符号将根据数据成员的系列(Exp1
、Exp2,
或Exp3
)而变化。因此,这一次,要描述数据所属的系列,就需要标记的颜色和形状。
首先,你需要在groupMarker
对象中给每个序列分配一个符号,如清单 24-8 所示。
清单 24-8。ch24_01b.html
var margin = {top: 70, right: 20, bottom: 40, left: 40},
w = 500 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var groupMarker = {
Exp1: "cross",
Exp2: "diamond",
Exp3: "triangle-down"
};
var color = d3.scale.category10();
然后,从代码中删除与点的表示有关的行(见清单 24-9)。这些行将被生成标记的其他行所替换(见清单 24-10)。
清单 24-9。ch24_01b.html
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.intensity); })
.style("fill", function(d) { return color(d.group); });
实际上,您将要生成的符号只不过是预定义的 SVG 路径。你可以从清单 24-10 中通过使用append("path")
函数来添加符号的事实中猜到这一点。相反,关于符号的生成,D3 库提供了一个特定的函数:d3.svg.symbol()
。要显示的符号作为参数通过type()
函数传递,例如,如果您想使用符号交叉使用type("cross")
。
然而,在这种情况下,要表示的符号是三个,并且它们取决于每个点的级数。因此,您必须通过应用于groupMarker,
的函数(d)
对所有数据进行迭代,这将返回对应于“十字形”、“菱形”和“向下三角形”符号的字符串。
最后,由 SVG 路径构成的符号也可以通过调整级联样式表(CSS)样式来更改。在本例中,您可以通过将fill
属性设置为white
来选择仅表示符号的轮廓。
清单 24-10。ch24_01b.html
d3.tsv("data_09.tsv", function(error, data) {
...
svg.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Intensity");
svg.selectAll("path")
.data(data)
.enter().append("path")
.
attr("transform", function(d) {
return "translate(" + x(d.time) + "," + y(d.intensity) + ")";
})
.attr("d", d3.svg.symbol().type( function(d) {
return groupMarker[d.group];
}))
.style("fill", "white")
.style("stroke", function(d) { return color(d.group); })
.style("stroke-width", "1.5px");
var legend = svg.selectAll(".legend")
...
});
图 24-2 显示了用各种符号代替点的散点图。
图 24-2。
In a scatterplot, the series could be represented by different symbols
使用定制标记
您已经看到 D3 库的标记只不过是 SVG 路径。您可以利用这一优势,通过创建其他符号来定制您的图表,这些符号将被添加到已定义的符号中。
在互联网上,你可以找到大量的 SVG 符号;一旦你决定了使用什么样的符号,你就可以得到它的路径,以便把它添加到你的网页中。更有事业心的读者也可以决定用 SVG 编辑器编辑 SVG 符号。建议你使用 Inkscape 编辑器(见图 24-3);可以从其官方网站: http://inkscape.org
下载。或者,更简单地说,您可以从一个已经设计好的 SVG 符号开始,然后根据您的喜好对其进行修改。为此,我推荐使用 SVG Tryit 页面的这个链接: www.w3schools.com/svg/tryit.asp?filename=trysvg_path
(见图 24-4 )。
图 24-4。
Tryit is a useful tool to preview SVG symbols in real time inserting the path
图 24-3。
Inkscape: a good SVG editor for generating symbols
因此,选择三个新符号(如新月、星星和火星符号)来替换默认符号。你提取它们的路径,然后插入到一个新对象的定义中,你称之为markers,
,如清单 24-11 所示。
清单 24-11。ch24_01c.html
var margin = {top: 70, right: 20, bottom: 40, left: 40},
w = 500 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var markers = {
mars: "m15,7 a 7,7 0 1,0 2,2 z l 1,1 7-7m-7,0 h 7 v 7",
moon: "m15,3 a 8.5,8.5 0 1,0 0,13 a 6.5,6.5 0 0,1 0,-13",
star: "m11,1 3,9h9l-7,5.5 2.5,8.5-7.5-5-7.5,5 2.5-8.5-7-6.5h9z"
};
var groupMarker = {
...
现在你必须更新在groupMarker
变量中定义的符号和组之间的关联,如清单 24-12 所示。
清单 24-12。ch24_01c.html
var groupMarker = {
Exp1: markers.star,
Exp2: markers.moon,
Exp3: markers.mars
};
当你创建 SVG 元素时,你可以做的最后一件事是改变路径的定义(见清单 24-13)。
清单 24-13。ch24_01c.html
svg.selectAll("path")
.data(data)
.enter().append("path")
.attr("transform", function(d) {
return "translate(" + x(d.time) + "," + y(d.intensity) + ")";
})
.attr("d", function(d) { return groupMarker[d.group]; })
.style("fill", "white")
.style("stroke", function(d) { return color(d.group); })
.style("stroke-width", "1.5px");
最后,你会得到一个散点图,报告你自己创造的或从网上下载的符号(见图 24-5 )。
图 24-5。
A scatterplot with a customized set of markers
添加更多功能
既然您已经学习了如何使用散点图来表示数据的分布,那么是时候介绍趋势线和聚类了。通常,详细分析数据分布中的一些点集,您可以看到它们遵循特定的趋势或倾向于聚集成簇。因此,用图形突出这一点将非常有用。在本节中,您将看到如何计算和表示线性趋势线的第一个示例。然后,您将看到第二个示例,该示例说明了如何突出显示 xy 平面中的一些集群的可能性。
趋势线
至于 jqPlot 库,其中有一个插件可以直接给出趋势线,而 D3 库不仅需要实现图形,还需要实现计算。
为简单起见,您将按照线性趋势计算一组点(一个序列)的趋势线。为此,您使用最小二乘法。这种方法确保在给定一组数据的情况下,尽可能通过最小化误差(误差的平方和)找到最符合点趋势的直线。
Note
要了解更多信息,我建议你访问 Wolfram MathWorld 的文章,网址为 http://mathworld.wolfram.com/LeastSquaresFitting.html
。
对于本例,您将继续使用散点图的代码,但不包括插入符号后所做的所有更改。为了避免不必要的错误和更多的重复,清单 24-14 显示了你需要用来作为这个例子的起点的代码。
清单 24-14。ch24_02.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="
http://d3js.org/d3.v3.js
<style>
body {
font: 16px sans-serif;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<script type="text/javascript">
var margin = {top: 70, right: 20, bottom: 40, left: 40},
w = 500 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var color = d3.scale.category10();
var x = d3.scale.linear()
.range([0, w]);
var y = d3.scale.linear()
.range([h, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" +margin.left+ "," +margin.top+ ")");
d3.tsv("data_09.tsv", function(error, data) {
data.forEach(function(d) {
d.time = +d.time;
d.intensity = +d.intensity;
});
x.domain(d3.extent(data, function(d) { return d.time; })).nice();
y.domain(d3.extent(data, function(d) { return d.intensity; })).nice();
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
svg.append("text")
.attr("class", "label")
.attr("x", w)
.attr("y", h + margin.bottom - 5)
.style("text-anchor", "end")
.text("Time [s]");
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Intensity");
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.intensity); })
.style("fill", function(d) { return color(d.group); });
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + (i * 20) + ")";
});
legend.append("rect")
.attr("x", w - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", w - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
});
var title = d3.select("svg").append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("class","title");
title.append("text")
.attr("x", (w / 2))
.attr("y", -30)
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("My Scatterplot");
</script>
</body></html>
首先,在tsv()
函数中定义所有用于最小二乘法的变量,如清单 24-15 所示。对于每个变量,您定义一个大小为 3 的数组,因为在您的图表中要表示三个系列。
清单 24-15。ch24_02.html
d3.tsv("data_09.tsv", function(error, data) {
sumx = [0,0,0];
sumy = [0,0,0];
sumxy = [0,0,0];
sumx2 = [0,0,0];
n = [0,0,0];
a = [0,0,0];
b = [0,0,0];
y1 = [0,0,0];
y2 = [0,0,0];
x1 = [9999,9999,9999];
x2 = [0,0,0];
colors = ["","",""];
data.forEach(function(d) {
...
});
现在你利用在数据解析过程中执行的数据迭代,同时计算最小二乘法所需的所有求和(见清单 24-16)。此外,直线的表示便于确定每个系列中的最大和最小 x 值。
清单 24-16。ch24_02.html
d3.tsv("data_09.tsv", function(error, data) {
...
data.forEach(function(d) {
d.time = +d.time;
d.intensity = +d.intensity;
for(var i = 0; i < 3; i=i+1)
{
if(d.group == "Exp"+(i+1)){
colors[i] = color(d.group);
sumx[i] = sumx[i] + d.time;
sumy[i] = sumy[i] + d.intensity;
sumxy[i] = sumxy[i] + (d.time * d.intensity);
sumx2[i] = sumx2[i] + (d.time * d.time);
n[i] = n[i] +1;
if(d.time < x1[i])
x1[i] = d.time;
if(d.time > x2[i])
x2[i] = d.time;
}
}
});
x.domain(d3.extent(data, function(d) { return d.time; })).nice();
...
});
一旦你计算了所有的总和,就该计算清单 24-17 中的最小二乘了。由于系列是三个,您将在一个for()
循环中重复计算三次。此外,在每个循环中,您直接插入 SVG 元素的创建,用于绘制与每个计算结果相对应的直线。
清单 24-17。ch24_02.html
d3.tsv("data_09.tsv", function(error, data) {
...
x.domain(d3.extent(data, function(d) { return d.time; })).nice();
y.domain(d3.extent(data, function(d) { return d.intensity; })).nice();
for(var i = 0; i < 3; i = i + 1){
b[i] = (sumxy[i] - sumx[i] * sumy[i] / n[i]) /
(sumx2[i] - sumx[i] * sumx[i] / n[i]);
a[i] = sumy[i] / n[i] - b[i] * sumx[i] / n[i];
y1[i] = b[i] * x1[i] + a[i];
y2[i] = b[i] * x2[i] + a[i];
svg.append("svg:line")
.attr("class","trendline")
.attr("x1", x(x1[i]))
.attr("y1", y(y1[i]))
.attr("x2", x(x2[i]))
.attr("y2", y(y2[i]))
.style("stroke", colors[i])
.style("stroke-width", 4);
}
现在你已经完成了全部,你可以看到散点图中三条趋势线的表示,如图 24-6 所示。
图 24-6。
Each group shows its trendline
簇
使用散点图时,可能需要执行聚类分析。在互联网上,有许多分析方法和算法,可以让您执行各种操作的识别和研究的集群。
聚类分析是一种分类技术,旨在识别数据分布(在这种情况下,是 xy 平面上的散点图)中的数据组(准确地说是聚类)。不同数据点在这些聚类中的分配不是事先定义的,但聚类分析的任务是确定选择和分组的标准。应尽可能区分这些聚类,在这种情况下,作为分组标准,聚类分析将基于各种数据点与代表聚类的质心点之间的距离(见图 24-7 )。
因此,这种分析的目的主要是识别数据分布中可能的相似性,在这方面,没有比散点图更合适的了,在散点图中,您可以根据聚类成员通过不同点的不同颜色来突出这些相似性。
在本节中,您将看到如何实现一个聚类分析算法,以及如何将它集成到一个散点图中。
图 24-7。
The cluster analysis groups a set of data points around a centroid for each cluster
k 均值算法
鉴于聚类分析的复杂性,本章将不详细讨论这一主题。您只对以不同于散点图所属系列的方式突出显示散点图的各个点感兴趣(Exp1、Exp2 和 Exp3)。在本例中,您希望根据数据点所属的分类为数据点着色。为此,您将使用一个简单的聚类分析案例:K-means 算法。首先定义要将所有数据分成的聚类数,然后为每个聚类选择一个代表点(质心)。每个数据点和三个质心之间的距离被认为是隶属度的标准。
互联网上有一些应用 K-means 方法的例子,它完全是用 JavaScript 实现的;其中我选择了一个由希瑟·亚瑟( https://github.com/harthur/clusterfck
)开发的,但是你可以用任何其他的来代替它。
对于这个例子,我冒昧地修改了代码,使其尽可能简单。从包含在 TSV 文件中的数据点开始,并在散点图中表示它们,您实际上是在分析这些点在 xy 空间中是如何分布的。例如,现在您有兴趣识别这个分布中的三个不同的集群。
为此,您将应用以下算法:
Make a random choice of three data points as cluster centroids. Iterate over all the data points in the file, assigning each of them to the cluster that has the closest centroid. At the end, you have all the data points divided into three clusters. Within each cluster, a new centroid is calculated, which this time will not correspond to any given point but will be the “midpoint” interposed between all points in the cluster. Recalculate steps 2 and 3 until the new centroids correspond to the previous ones (that is, the coordinates of the centroids in the xy plane remain unchanged).
一旦算法完成,散点图中的点将具有三种不同的颜色,对应于三个不同的聚类。
请注意,在这个算法中没有优化,因此每次在浏览器中上传页面时,结果总是不同的。其实你每次得到的都是一个可能的解,而不是“最佳解”。
现在为了保持一定的模块性,您将在一个外部文件中编写集群分析的代码,您将称之为kmeans.js
。
首先,您将实现randomCentroids()
函数,该函数将从包含在文件中的点(这里在points
数组中传递)中选择k
个点(在本例中,k
= 3 ),将它们指定为 k 个簇的质心(参见清单 24-18)。该函数对应于算法的点 1。
清单 24-18。kmeans.js
function randomCentroids(points, k) {
var centroids = points.slice(0);
centroids.sort(function() {
return (Math.round(Math.random()) - 0.5);
});
return centroids.slice(0, k);
}:
现在,您必须将文件中包含的所有点分配给三个不同的集群。为此,您需要计算每个数据点和所讨论的质心之间的距离,因此您需要实现一个特定的函数来计算两点之间的距离。在清单 24-19 中,定义了distance()
函数,它返回v1
和v2
普通点之间的距离。
清单 24-19。kmeans.js
function distance(v1, v2) {
var total = 0;
for (var i = 0; i < v1.length; i++) {
total += Math.pow((v2[i] - v1[i]), 2);
}
return Math.sqrt(total);
};
现在您已经知道如何计算两点之间的距离,您可以实现一个函数来决定每个数据点的聚类分配,计算它与所有质心的距离并选择较小的一个。因此,您可以将closestCentroid()
函数添加到代码中,如清单 24-20 所示。
清单 24-20。kmeans.js
function closestCentroid(point, centroids) {
var min = Infinity;
var index = 0;
for (var i = 0; i < centroids.length; i++) {
var dist = distance(point, centroids[i]);
if (dist < min) {
min = dist;
index = i;
}
}
return index;
}:
现在,您可以编写完整表达首次公开的算法的函数。这个函数需要两个参数,输入数据点(points
)和它们将被分成的聚类数(k
)(见清单 24-21)。在里面,你可以使用新实现的randomCentroids()
函数选择质心(算法的第一点)。
清单 24-21。kmeans.js
function kmeans(points, k) {
var centroids = randomCentroids(points, k);
};
一旦你选择了三个质心,你可以将所有的数据点(包含在points
数组中)分配给三个簇,定义assignment
数组,如清单 24-22 所示(算法的第 2 点)。该数组的长度与 points 数组的长度相同,其元素的顺序对应于数据点的顺序。每个元素都包含它们所属的集群的编号。例如,如果在assignment
数组的第三个元素中你有一个值 2,那么这将意味着第三个数据点属于第三个簇(簇是 0、1 和 2)。
清单 24-22。kmeans.js
function kmeans(points, k) {
var centroids = randomCentroids(points, k);
var assignment = new Array(points.length);
var clusters = new Array(k);
var movement = true;
while (movement) {
for (var i = 0; i < points.length; i++) {
assignment[i] = closestCentroid(points[i], centroids);
}
movement = false;
}
return clusters;
};
最后,通过一次选择一个聚类,您将重新计算质心,并重复整个过程,直到您总是获得相同的值。首先,如清单 24-23 所示,通过迭代器j
进行迭代,一次分析一个集群。在它的内部,基于assignment
数组的内容,用属于该集群的所有数据点填充assigned
数组。这些值用于计算在newCentroid
变量中定义的新质心。要确定它的新坐标[x,y],需要分别对该簇所有点的所有 x 和 y 值求和。然后将这些量除以点数,因此新质心的 x 和 y 值只不过是所有坐标的平均值。
要做到这一切,你需要用g
和i
迭代器实现一个双重迭代(两个for()
循环)。在g
上的迭代允许你一次处理一个坐标(首先是 x,然后是 y,等等),而在i
上的迭代允许你逐点求和,以便进行求和。
如果新的质心不同于先前的质心,那么再次重复将各种数据点分配给聚类,并且循环再次开始(算法的步骤 3 和 4)。
清单 24-23。kmeans.js
function kmeans(points, k) {
...
while (movement) {
for (var i = 0; i < points.length; i++) {
assignment[i] = closestCentroid(points[i], centroids);
}
movement = false;
for (var j = 0; j < k; j++) {
var assigned = [];
for (var i = 0; i < assignment.length; i++) {
if (assignment[i] == j) {
assigned.push(points[i]);
}
}
if (!assigned.length) {
continue;
}
var centroid = centroids[j];
var newCentroid = new Array(centroid.length);
for (var g = 0; g < centroid.length; g++) {
var sum = 0;
for (var i = 0; i < assigned.length; i++) {
sum += assigned[i][g];
}
newCentroid[g] = sum / assigned.length;
if (newCentroid[g] != centroid[g]) {
movement = true;
}
}
centroids[j] = newCentroid;
clusters[j] = assigned;
}
}
return clusters;
};
将聚类分析应用于散点图
结束了用于聚类分析的 JavaScript 代码之后,是时候回到 web 页面了。正如您对趋势线示例所做的那样,您将使用如清单 24-24 所示的散点图代码。这是您进行集成聚类分析所需的各种更改和添加的起点。
清单 24-24。ch24_03.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="
http://d3js.org/d3.v3.js
<style>
body {
font: 16px sans-serif;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<script type="text/javascript">
var margin = {top: 70, right: 20, bottom: 40, left: 40},
w = 500 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var color = d3.scale.category10();
var x = d3.scale.linear()
.range([0, w]);
var y = d3.scale.linear()
.range([h, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" +margin.left+ "," +margin.top+ ")");
d3.tsv("data_09.tsv", function(error, data) {
data.forEach(function(d) {
d.time = +d.time;
d.intensity = +d.intensity;
});
x.domain(d3.extent(data, function(d) { return d.time; })).nice();
y.domain(d3.extent(data, function(d) { return d.intensity; })).nice();
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
svg.append("text")
.attr("class", "label")
.attr("x", w)
.attr("y", h + margin.bottom - 5)
.style("text-anchor", "end")
.text("Time [s]");
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("text")
.attr("class", "label")
.attr("transform", "rotate(–90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Intensity");
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + (i * 20) + ")";
});
legend.append("rect")
.attr("x", w - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", w - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
});
var title = d3.select("svg").append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("class","title");
title.append("text")
.attr("x", (w / 2))
.attr("y", -30)
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("My Scatterplot");
</script>
</body>
</html>
首先,您需要包含您刚刚创建的文件kmeans.js
,以便使用其中定义的函数(参见清单 24-25)。
清单 24-25。ch24_03.html
...
<meta charset="utf-8">
<script src="
http://d3js.org/d3.v3.js
<
script src="./kmeans.js"></script>
<style>
body {
...
准备一个保存待分析数据的数组,并将其命名为myPoints
。一旦完成,你就可以添加对kmean()
函数的调用了,如清单 24-26 所示。
清单 24-26。ch24_03.html
d3.tsv("data_09.tsv", function(error, data) {
var myPoints = [];
data.forEach(function(d) {
d.time = +d.time;
d.intensity = +d.intensity;
myPoints.push([d.time, d.intensity]);
});
var clusters = kmeans(myPoints, 3);
x.domain(d3.extent(data, function(d) { return d.time; })).nice();
y.domain(d3.extent(data, function(d) { return d.intensity; })).nice();
...
};
最后,修改 circle SVG 元素的定义,使这些元素在由kmeans()
函数返回的结果的基础上被表示出来,如清单 24-27 所示。
清单 24-27。ch24_03.html
d3.tsv("data_09.tsv", function(error, data) {
...
svg.append("text")
.attr("class", "label")
.attr("transform", "rotate(–90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Intensity");
for(var i = 0; i < 3; i = i + 1){
svg.selectAll(".dot" + i)
.data(clusters[i])
.enter().append("circle")
.attr("class", "dot")
.attr("r", 5)
.attr("cx", function(d) { return x(d[0]); })
.attr("cy", function(d) { return y(d[1]); })
.style("fill", function(d) { return color(i); });
}
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
...
};
在图 24-8 中,你可以看到聚类分析后可能获得的结果之一的表示。
图 24-8。
The scatterplot shows one possible solution of the clustering analysis applied to the data in the TSV file
突出显示数据点
另一个你还没有在 D3 库中涉及到的功能,但是你已经在 jqPlot 库中看到了(见第十章)高亮显示和与之相关的事件。D3 库甚至允许您将此功能添加到图表中,并以与 jqPlot 库非常相似的方式处理事件。
D3 库提供了一个特殊的函数来激活或删除事件监听器:on()
函数。这个函数通过链接方法直接应用于选择,通常需要两个参数:类型和侦听器。
selection.``on(``type, listener
第一个参数是你想要激活的事件的类型,它被表示为一个包含事件名称的字符串(比如mouseover
、submit
等)。).第二个参数通常由一个函数组成,该函数充当侦听器,并在事件被触发时执行操作。
基于所有这些,如果您想要添加突出显示功能,您需要管理两个特定的事件:一个是当用户通过突出显示将鼠标悬停在数据点上时,另一个是当用户将鼠标从数据点上移开时,将它恢复到正常状态。这两个事件在 D3 库中定义为mouseover
和mouseout
。现在,您必须将这些事件加入到两个不同的动作中。使用mouseover
,你将扩大数据点的体积,你将增加它的颜色的鲜艳度,以进一步与其他的进行对比。相反,您将使用mouseout
执行完全相反的操作,恢复原始数据点的颜色和大小。
清单 24-28 显示了应用于散点图代码的高亮功能。
清单 24-28。ch24_04.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="
http://d3js.org/d3.v3.js
<style>
body {
font: 16px sans-serif;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<script type="text/javascript">
var margin = {top: 70, right: 20, bottom: 40, left: 40},
w = 500 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var color = d3.scale.category10();
var x = d3.scale.linear()
.range([0, w]);
var y = d3.scale.linear()
.range([h, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" +margin.left+ "," +margin.top+ ")");
d3.tsv("data_09.tsv", function(error, data) {
data.forEach(function(d) {
d.time = +d.time;
d.intensity = +d.intensity;
});
x.domain(d3.extent(data, function(d) { return d.time; })).nice();
y.domain(d3.extent(data, function(d) { return d.intensity; })).nice();
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
svg.append("text")
.attr("class", "label")
.attr("x", w)
.attr("y", h + margin.bottom - 5)
.style("text-anchor", "end")
.text("Time [s]");
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("text")
.attr("class", "label")
.attr("transform", "rotate(–90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Intensity");
var dots = svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 5)
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.intensity); })
.style("fill", function(d) { return color(d.group); })
.on("mouseover", function() { d3.select(this)
.style("opacity",1.0)
.attr("r", 15);
})
.on("mouseout", function() { d3.select(this)
.style("opacity",0.6)
.attr("r", 5);
}) ;
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + (i * 20) + ")";
});
legend.append("rect")
.attr("x", w - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", w - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
});
var title = d3.select("svg").append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("class","title");
title.append("text")
.attr("x", (w / 2))
.attr("y", –30)
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("My Scatterplot");
</script>
</body>
</html>
在加载网页以查看结果之前,您需要通过设置 CSS 样式中的opacity
属性来使数据点的所有颜色变暗,如清单 24-29 所示。
清单 24-29。ch24_04.html
<style>
body {
font: 16px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.dot {
stroke: #000;
opacity: 0.6;
}
</style>
图 24-9 显示了气泡图中处于两种不同状态的多个数据点之一。在左侧,您可以看到数据点处于正常状态,而在右侧,它被突出显示。
图 24-9。
A bubble assumes two states: normal on the left and highlighted when moused over on the right
泡泡图
只需对之前的散点图示例进行一些修改,就可以非常容易地构建一个气泡图。首先,您需要向数据中添加一个新列。在这种情况下(见清单 24-30),您将带宽值作为最后一列添加到data_09.tsv
中,并将其保存为data_10.tsv
。
清单 24-30。data_10.tsv
time intensity group bandwidth
10 171.11 Exp1 20
14 180.31 Exp1 30
17 178.32 Exp1 10
42 173.22 Exp3 40
30 145.22 Exp2 35
30 155.68 Exp3 80
23 200.56 Exp2 10
15 192.33 Exp1 30
24 173.22 Exp2 10
20 203.78 Exp2 20
18 187.88 Exp1 60
45 180.00 Exp3 10
27 181.33 Exp2 40
16 198.03 Exp1 30
47 179.11 Exp3 20
27 175.33 Exp2 30
28 162.55 Exp2 10
24 208.97 Exp1 10
23 200.47 Exp1 10
43 165.08 Exp3 10
27 168.77 Exp2 20
23 193.55 Exp2 50
19 188.04 Exp1 10
40 170.36 Exp3 40
21 184.98 Exp2 20
15 197.33 Exp1 30
50 188.45 Exp3 10
23 207.33 Exp1 10
28 158.60 Exp2 10
29 151.31 Exp2 30
26 172.01 Exp2 20
23 191.33 Exp1 10
25 226.11 Exp1 10
60 198.33 Exp3 10
现在,数据列表中有了第三个参数,对应于新的色谱柱带宽。这个值用一个数字表示,为了读取它,你需要在数据解析中添加bandwidth
变量,如清单 24-31 所示。你一定不要忘记在tsv()
函数中用data_10.tsv
替换 TSV 文件的名称。
清单 24-31。ch24_05.html
d3.tsv("``data_10.tsv
var myPoints = [];
data.forEach(function(d) {
d.time = +d.time;
d.intensity = +d.intensity;
d.bandwidth = +d.bandwidth;
myPoints.push([d.time, d.intensity]);
});
...
});
现在你可以通过增加半径将所有的点变成圆形区域,因为它们已经被设置为 SVG 元素<circle>
,如清单 24-32 所示。这些圆的半径必须与带宽值成比例,因此可以直接分配给r
属性。值 0.4 是一个校正因子,它适合在气泡图中很好地表示带宽值(在其他情况下,您将需要使用其他值作为因子)。
清单 24-32。ch24_05.html
d3.tsv("data_10.tsv", function(error, data) {
...
svg.append("text")
.attr("class", "label")
.attr("transform", "rotate(–90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Intensity");
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", function(d) { return d.bandwidth * 0.4 })
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.intensity); })
.style("fill", function(d) { return color(d.group); })
.on("mouseover", function() { d3.select(this)
.style("opacity",1.0)
.attr("r", function(d) { return d.bandwidth * 0.5 });
})
.on("mouseout", function() { d3.select(this)
.style("opacity",0.6)
.attr("r", function(d) { return d.bandwidth * 0.4 });
});
var legend = svg.selectAll(".legend")
...
});
最后但同样重要的是,您需要更新新图表的标题,如清单 24-33 所示。
清单 24-33。ch24_05.html
title.append("text")
.attr("x", (w / 2))
.attr("y", –30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.
text("My Bubble Chart");
而图 24-10 将是结果。
图 24-10。
A bubble chart
摘要
在这一章中,你简要地看到了如何用 D3 库生成气泡图和散点图。甚至在这里,你使用 jqPlot 库执行了你在本书第二部分看到的相同类型的图表。因此,您可以对这两个不同的库以及实现相同类型图表的各自方法有所了解。
在下一章中,你将实现一种你在书中还没有涉及到的图表类型:雷达图。这个表示的例子用 jqPlot 是不可行的,但是由于 D3 图形元素,它是可能实现的。因此,下一章将是一个很好的例子,说明如何利用 D3 库的潜力来开发不同于最常见的图表的其他类型的图表。
二十五、D3 雷达图
Abstract
本章介绍了一种你还没有读过的图表:雷达图。首先,您将了解它是什么,包括它的基本特性,以及如何使用 D3 库提供的 SVG 元素创建一个。
本章介绍了一种你还没有读过的图表:雷达图。首先,您将了解它是什么,包括它的基本特性,以及如何使用 D3 库提供的 SVG 元素创建一个。
首先,从一个 CSV 文件中读取少量有代表性的数据。然后,参考这些数据,您将看到如何一步一步地实现雷达图的所有组件。在本章的第二部分,您将使用相同的代码从更复杂的文件中读取数据,其中要处理的系列数和数据量都更大。当您需要从头开始表示一种新类型的图表时,这种方法是一种相当常见的做法。您从一个简单但完整的例子开始,然后,一旦您实现了基本的例子,您将使用更复杂和真实的数据来扩展它。
雷达图
雷达图也称为网状图或蜘蛛图,因为它们呈现典型的网状结构(见图 25-1 )。它们是二维图表,使您能够表示三个或更多的定量变量。它们由一系列角度相同的辐条组成,每个辐条代表一个变量。每个轮辐上都有一个点,该点离中心的距离与给定变量的大小成正比。然后,一条线连接每个辐条上报告的点,从而给绘图一个网状外观。如果没有这条连接线,图表看起来会更像一个扫描雷达。
图 25-1。
A radar chart looks like a spider web
构建自动缩放轴
复制下面的数据,并将其保存在一个文件中,名为data_11.csv
(见清单 25-1)。
清单 25-1。data_11.csv
section,set1,set2,
A,1,6,
B,2,7,
C,3,8,
D,4,9,
E,5,8,
F,4,7,
G,3,6,
H,2,5,
在清单 25-2 中,您定义了绘图区域和边距。然后用category10()
功能创建一个颜色序列。
清单 25-2。ch25_01.html
var margin = {top: 70, right: 20, bottom: 40, left: 40},
w = 500 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var color = d3.scale.category20();
在刚刚定义的绘图区域中,您还必须定义一个可以容纳圆形的特定区域,在本例中是雷达图。一旦定义了这个区域,就像笛卡尔轴一样定义了半径。事实上,雷达图上的每个辐条都被认为是一个轴,在这个轴上放置一个变量。因此,在半径上定义一个线性标度,如清单 25-3 所示。
清单 25-3。ch25_01.html
var circleConstraint = d3.min([h, w]);
var radius = d3.scale.linear()
.range([0, (circleConstraint / 2)]);
您需要找到绘图区域的中心。这是雷达图的中心,也是所有辐条的辐射点(见清单 25-4)。
清单 25-4。ch25_01.html
var centerXPos = w / 2 + margin.left;
var centerYPos = h / 2 + margin.top;
开始绘制根元素<svg>
,如清单 25-5 所示。
清单 25-5。ch25_01.html
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + centerXPos + ", " + centerYPos + ")");
现在,如清单 25-6 所示,用d3.csv()
函数读取文件内容。您需要验证读取值set1
和set2
是否被解释为数值。您还想知道所有这些值中的最大值,以便定义一个根据其值扩展的刻度。
清单 25-6。ch25_01.html
d3.csv("data_11.csv", function(error, data) {
var maxValue = 0;
data.forEach(function(d) {
d.set1 = +d.set1;
d.set2 = +d.set2;
if(d.set1 > maxValue)
maxValue = d.set1;
if(d.set2 > maxValue)
maxValue = d.set2;
});
});
知道输入数据的最大值后,将满量程值设置为该最大值乘以 1.5。在这种情况下,您必须手动定义轴上的记号,而不是使用自动生成的记号。事实上,这些扁虱是球形的,因此具有非常独特的特征。此示例将半径轴的范围分为五个刻度。一旦定义了记号的值,就可以给半径轴分配一个域(见清单 25-7)。
清单 25-7。ch25_01.html
d3.csv("data_11.csv", function(error, data) {
...
data.forEach(function(d) {
...
});
var topValue = 1.5 * maxValue;
var ticks = [];
for(i = 0; i < 5;i += 1){
ticks[i] = topValue * i / 5;
}
radius.domain([0,topValue]);
});
现在你已经有了所有的数值,我们可以嵌入一些<svg>
元素来设计一个雷达网格,它的形状和值会根据输入的数据而变化(见清单 25-8)。
清单 25-8。ch25_01.html
d3.csv("data_11.csv", function(error, data) {
...
radius.domain([0,topValue]);
var circleAxes = svg.selectAll(".circle-ticks")
.data(ticks)
.enter().append("g")
.attr("class", "circle-ticks");
circleAxes.append("svg:circle")
.attr("r", function(d) {return radius(d);})
.attr("class", "circle")
.style("stroke", "#CCC")
.style("fill", "none");
circleAxes.append("svg:text")
.attr("text-anchor", "middle")
.attr("dy", function(d) {return radius(d)})
.text(String);
});
您已经创建了一个名为circle-ticks
的五个<g>
标签的结构,如图 25-2 所示,每个标签包含一个<circle>
元素(绘制网格)和一个<text>
元素(显示相应的数值)。
图 25-2。
FireBug shows how the circle-ticks
are structured
所有这些代码生成了如图 25-3 所示的圆形网格。
图 25-3。
The circular grid of a radar chart
如您所见,分笔成交点上报告的值将根据数据中包含的最大值而变化。
现在是画轮辐的时候了,和data_11.csv
文件中的线条一样多的射线。这些行中的每一行都对应一个变量,变量的名字被输入到文件的第一列中(见清单 25-9)。
清单 25-9。ch25_01.html
d3.csv("data_11.csv", function(error, data) {
...
circleAxes.append("svg:text")
.attr("text-anchor", "middle")
.attr("dy", function(d) {return radius(d)})
.text(String);
lineAxes = svg.selectAll('.line-ticks')
.data(data)
.enter().append('svg:g')
.attr("transform", function (d, i) {
return "rotate(" + ((i / data.length * 360) - 90) +
")translate(" + radius(topValue) + ")";
})
.attr("class", "line-ticks");
lineAxes.append('svg:line')
.
attr("x2", -1 * radius(topValue))
.style("stroke", "#CCC")
.style("fill", "none");
lineAxes.append('svg:text')
.
text(function(d) { return d.section; })
.attr("text-anchor", "middle")
.attr("transform", function (d, i) {
return "rotate("+(90 - (i * 360 / data.length)) + ")";
});
});
现在图表中显示了辐条,如图 25-4 所示。
图 25-4。
The radial axes of a radar chart
向雷达图添加数据
现在是时候考虑文件中的数字列了。每一列都可以被视为一个系列,每个系列都必须分配一种颜色。您可以通过从文件中取出标题并删除第一列来定义系列。然后你可以根据系列的顺序创建颜色域,然后定义绘制它们的线条(见清单 25-10)。
清单 25-10。ch25_01.html
d3.csv("data_11.csv", function(error, data) {
...
lineAxes.append('svg:text')
.text(function(d) { return d.section; })
.attr("text-anchor", "middle")
.attr("transform", function (d, i) {
return "rotate("+(90-(i*360/data.length))+")";
});
var series = d3.keys(data[0])
.filter(function(key) { return key !== "section"; })
.filter(function(key) { return key !== ""; });
color.domain(series);
var lines = color.domain().map(function(name){
return (data.concat(data[0])).map(function(d){
return +d[name];
});
});
});
这是series
数组的内容:
[ "set1", "set2" ]
这是lines
数组的内容:
[[1,2,3,...],[6,7,8,...]]
这些线将帮助您创建相应的路径元素,并使您能够在雷达图上绘制系列的趋势。每个序列都将通过不同轮辐中的假设值(见清单 25-11)。
清单 25-11。ch25_01.html
d3.csv("data_11.csv", function(error, data) {
...
var lines = color.domain().map(function(name){
return (data.concat(data[0])).map(function(d){
return +d[name];
});
});
var sets = svg.selectAll(".series")
.data(series)
.enter().append("g")
.attr("class", "series");
sets.append('svg:path')
.data(lines)
.attr("class", "line")
.attr("d", d3.svg.line.radial()
.radius(function (d) {
return radius(d);
})
.angle(function (d, i) {
if (i == data.length) {
i = 0;
} //close the line
return (i / data.length) * 2 * Math.PI;
}))
.data(series)
.style("stroke-width", 3)
.style("fill","none")
.style("stroke", function(d,i){
return color(i);
});
});
您还可以添加一个显示系列名称的图例(实际上是列的标题)和一个标题放在绘图区域的顶部,如清单 25-12 所示。
清单 25-12。ch25_01.html
d3.csv("data_11.csv", function(error, data) {
...
.data(series)
.style("stroke-width", 3)
.style("fill","none")
.style("stroke", function(d,i){
return color(i);
});
var legend = svg.selectAll(".legend")
.data(series)
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + i * 20 + ")";
});
legend.append("rect")
.attr("x", w/2 -18)
.attr("y", h/2 - 60)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d,i){ return color(i);});
legend.append("text")
.attr("x", w/2 -24)
.attr("y", h/2 - 60)
.attr("dy","1.2em")
.style("text-anchor", "end")
.text(function(d) { return d; });
});
var title = d3.select("svg").append("g")
.attr("transform", "translate(" +margin.left+ "," +margin.top+ ")")
.attr("class","title");
title.append("text")
.attr("x", (w / 2))
.attr("y", -30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("My Radar Chart");
最后但同样重要的是,您可以添加一个 CSS 样式类来控制文本样式,如清单 25-13 所示。
清单 25-13。ch25_01.html
<style>
body {
font: 16px sans-serif;
}
</style>
图 25-5 显示了生成的雷达图。
图 25-5。
A radar chart with two series
改善你的雷达图
如果您按照前面的示例进行操作,那么向雷达图添加更多的列和行应该不会有任何问题。打开最后一个输入数据文件,名为data_11.csv
,再添加两列和两行。将文件另存为data_12.csv
,如清单 25-14 所示。
清单 25-14。data_12.csv
section,set1,set2,set3,set4,
A,1,6,2,10,
B,2,7,2,14,
C,3,8,1,10,
D,4,9,4,1,
E,5,8,7,2,
F,4,7,11,1,
G,3,6,14,2,
H,2,5,2,1,
I,3,4,5,2,
L,1,5,1,2,
现在您必须在d3.csv()
函数中用data12.csv
文件替换对data11.csv
文件的调用,如清单 25-15 所示。
清单 25-15。ch25_02.html
d3.csv("``data_12.csv
...});
图 25-6 显示了结果。
图 25-6。
A radar chart with four series
哇,成功了!准备好添加另一个特性了吗?到目前为止,您已经描绘了一条穿过各种辐条的线,循环返回到起点;趋势现在描述了一个特定的领域。您通常会对雷达图中由不同线条分隔的区域比对线条本身更感兴趣。如果你想对你的雷达图做这个小小的转换以显示区域,你只需要再增加一条路径,如清单 25-16 所示。这个路径实际上与已经存在的路径相同,只是这个新路径没有绘制代表系列的线条,而是对内部封闭的区域进行了着色。在本例中,您将使用相应线条的颜色,但增加一点透明度,以免覆盖底层系列。
清单 25-16。ch25_02.html
d3.csv("``data_12.csv
...
var sets = svg.selectAll(".series")
.data(series)
.enter().append("g")
.attr("class", "series");
sets.append('svg:path')
.data(lines)
.attr("class", "line")
.attr("d", d3.svg.line.radial()
.radius(function (d) {
return radius(d);
})
.angle(function (d, i) {
if (i == data.length) {
i = 0;
}
return (i / data.length) * 2 * Math.PI;
}))
.data(series)
.style("stroke-width", 3)
.style("opacity", 0.4)
.style("fill",function(d,i){
return color(i);
})
.style("stroke", function(d,i){
return color(i);
})
;
sets.append('svg:path')
.data(lines)
.attr("class", "line")
.attr("d", d3.svg.line.radial()
...
});
正如你在图 25-7 中看到的,你现在有了一个半透明区域的雷达图。
图 25-7。
A radar chart with color filled areas
摘要
本章解释了如何实现雷达图。这种类型的图表用 jqPlot 是不可行的,所以这一章是有用的,部分是为了突出 D3 库的潜力。它展示了一个示例,帮助您理解如何开发不同于最常见类型的其他图表。
下一章也是最后一章通过考虑两个不同的案例来结束这本书。这些案例旨在以简化的方式提出开发人员在处理真实数据时必须面对的典型情况。在第一个例子中,您将看到如何使用 D3 来表示实时生成或获取的数据。您将创建一个不断更新的图表,始终显示当前情况。在第二个例子中,您将使用 D3 库来读取数据库中包含的数据。
二十六、使用 D3 处理实时数据
Abstract
你已经看到了如何用 jqPlot 处理实时图表,在这一章中,你将使用 D3 库实现同样的例子。实际上,您将创建一个折线图,显示模拟外部数据源的函数所生成的实时值。数据会不断生成,因此折线图也会相应变化,始终显示最新情况。
你已经看到了如何用 jqPlot 处理实时图表,在这一章中,你将使用 D3 库实现同样的例子。实际上,您将创建一个折线图,显示模拟外部数据源的函数所生成的实时值。数据会不断生成,因此折线图也会相应变化,始终显示最新情况。
在本章的第二部分,你将制作一个稍微复杂一点的图表。这一次,您将使用一个例子,其中的数据源是一个真实的数据库。首先,您将实现一个折线图,它将读取外部文件中包含的数据。稍后,您将学习如何使用该示例来读取相同的数据,但这次是从数据库的表中读取。
实时图表
您有一个模拟函数的数据源,该函数返回变量性能的随机变化。这些值存储在一个具有缓冲区功能的数组中,其中只包含最近的 10 个值。对于生成或获取的每个输入值,最旧的值将被新值替换。此数组中包含的数据显示为每 3 秒更新一次的折线图。点击一个按钮就可以激活一切。
让我们开始设置表示折线图的基础(查看用 D3 库开发折线图,见第二十章)。首先,你写一个 HTML 结构,你将在这个结构上构建你的图表,如清单 26-1 所示。
清单 26-1。ch26_01.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="
http://d3js.org/d3.v3.js
<style>
//add the CSS styles here
</style>
<body>
<script type="text/javascript">
// add the JavaScript code here
</script>
</body>
</html>
现在,从输入数据的管理开始,您开始定义在代码编写过程中对您有帮助的变量。如前所述,您将要在图表上表示的数据来自一个函数,该函数从一个初始值开始产生随机变量,可以是正的,也可以是负的。你决定从 10 点开始。
因此,从这个值开始,你从随机函数接收一个要在一个数组中收集的值序列,你将称之为data
(见清单 26-2)。现在,你只在起始值(10)内给它赋值。给定这个数组,要实时接收值,您需要设置一个最大限制,在本例中是 11 个元素(0–10)。填充后,您将把数组作为一个队列来管理,其中最旧的项将被删除,以便为新项腾出空间。
清单 26-2。ch26_01.html
<script type="text/javascript">
var data = [10];
w = 400;
h = 300;
margin_x = 32;
margin_y = 20;
ymax = 20;
ymin = 0;
y = d3.scale.linear().domain([ymin, ymax]).range([0 + margin_y, h - margin_y]);
x = d3.scale.linear().domain([0, 10]).range([0 + margin_x, w - margin_x]);
</script>
包含在data
数组中的值将沿着 y 轴表示,因此有必要建立刻度标签必须覆盖的值的范围。例如,从值 10 开始,您可以决定此范围涵盖从 0 到 20 的值(稍后,您将确保刻度对应于 5 的倍数范围,即 0、5、10、15 和 20)。因为要显示的值是随机生成的,所以它们将逐渐假定值甚至大于 20,如果是这样,您将看到图表顶部边缘的线条消失了。怎么办?
因为主要目标是创建一个在获取新数据后自动重绘的图表,所以您将确保即使是刻度标签也根据包含在data
数组中的值所覆盖的范围进行调整。为了实现这一点,您需要用ymax
和ymin
变量定义y
范围,用x
范围覆盖静态范围[0–10]。
w
和h
变量(宽度和高度)定义了您将在其上绘制折线图的绘图区域的大小,而margin_x
和margin_y
允许您调整边距。
现在,让我们创建代表图表各个部分的标量矢量图形(SVG)元素。首先创建<svg>
根,然后定义 x 和 y 轴,如清单 26-3 所示。
清单 26-3。ch26_01.html
<script type="text/javascript">
...
y = d3.scale.linear().domain([ymin, ymax]).range([0 + margin_y, h - margin_y]);
x = d3.scale.linear().domain([0, 10]).range([0 + margin_x, w - margin_x]);
var svg = d3.select("body")
.append("svg:svg")
.attr("width", w)
.attr("height", h);
var g = svg.append("svg:g")
.attr("transform", "translate(0," + h + ")");
// draw the x axis
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -y(0))
.attr("x2", x(w))
.attr("y2", -y(0));
// draw the y axis
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -y(0))
.attr("x2", x(0))
.attr("y2", -y(25));
</script>
接下来,在两个轴上添加记号和相应的标签,然后添加栅格。最后,用line()
函数在图表上画线(见清单 26-4)。
清单 26-4。ch26_01.html
<script type="text/javascript">
...
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -y(0))
.attr("x2", x(0))
.attr("y2", -y(25));
//draw the xLabels
g.selectAll(".xLabel")
.data(x.ticks(5))
.enter().append("svg:text")
.attr("class", "xLabel")
.text(String)
.attr("x", function(d) { return x(d) })
.attr("y", 0)
.attr("text-anchor", "middle");
// draw the yLabels
g.selectAll(".yLabel")
.data(y.ticks(5))
.enter().append("svg:text")
.attr("class", "yLabel")
.text(String)
.attr("x", 25)
.attr("y", function(d) { return -y(d) })
.attr("text-anchor", "end");
//draw the x ticks
g.selectAll(".xTicks")
.data(x.ticks(5))
.enter().append("svg:line")
.attr("class", "xTicks")
.attr("x1", function(d) { return x(d); })
.attr("y1", -y(0))
.attr("x2", function(d) { return x(d); })
.attr("y2", -y(0) - 5);
// draw the y ticks
g.selectAll(".yTicks")
.data(y.ticks(5))
.enter().append("svg:line")
.attr("class", "yTicks")
.attr("y1", function(d) { return -1 * y(d); })
.attr("x1", x(0) + 5)
.attr("y2", function(d) { return -1 * y(d); })
.attr("x2", x(0))
//draw the x grid
g.selectAll(".xGrids")
.data(x.ticks(5))
.enter().append("svg:line")
.attr("class", "xGrids")
.attr("x1", function(d) { return x(d); })
.attr("y1", -y(0))
.attr("x2", function(d) { return x(d); })
.attr("y2", -y(25));
// draw the y grid
g.selectAll(".yGrids")
.data(y.ticks(5))
.enter().append("svg:line")
.attr("class", "yGrids")
.attr("y1", function(d) { return -1 * y(d); })
.attr("x1", x(w))
.attr("y2", function(d) { return -y(d); })
.attr("x2", x(0));
var line = d3.svg.line()
.x(function(d,i) { return x(i); })
.y(function(d) { return -y(d); })
</script>
为了给你的图表一个令人愉快的外观,定义层叠样式表(CSS)样式也是必要的,如清单 26-5 所示。
清单 26-5。ch26_01.html
<style>
path {
stroke: steelblue;
stroke-width: 3;
fill: none;
}
line {
stroke: black;
}
.xGrids {
stroke: lightgray;
}
.yGrids {
stroke: lightgray;
}
text {
font-family: Verdana;
font-size: 9pt;
}
</style>
现在,您在图表上方添加一个按钮,确保当用户点击它时updateData()
功能被激活,如清单 26-6 所示。
清单 26-6。ch26_01.html
< body>
<div id="option">
<input name="updateButton"
type="button"
value="Update"
onclick="updateData()" />
</div>
然后,你实现getRandomInt()
函数,它产生一个介于最小值和最大值之间的随机整数值(见清单 26-7)。
清单 26-7。ch26_01.html
function getRandomInt (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
清单 26-8 显示了updateData()
函数,其中由getRandomInt()
函数生成的值被添加到数组的最近值中,以模拟趋势的变化。这个新值存储在数组中,而最旧的值被删除;因此,数组的大小总是保持不变。
清单 26-8。ch26_01.html
function updateData() {
var last = data[data.length-1];
if(data.length > 10){
data.shift();
}
var newlast = last + getRandomInt(-3,3);
if(newlast < 0)
newlast = 0;
data.push(newlast);
};
如果由getRandomInt()
函数返回的新值大于或小于 y 轴上表示的范围,您将看到数据行延伸到图表的边缘。为了防止这种情况发生,你必须通过改变ymin
和ymax
变量来改变 y 轴上的间隔,并用这些新值更新y
范围,如清单 26-9 所示。
清单 26-9。ch26_01.html
function updateData() {
...
if(newlast < 0)
newlast = 0;
data.push(newlast);
if(newlast > ymax){
ymin = ymin + (newlast - ymax);
ymax = newlast;
y = d3.scale.linear().domain([ymin, ymax])
.range([0 + margin_y, h - margin_y]);
}
if(newlast < ymin){
ymax = ymax - (ymin - newlast);
ymin = newlast;
y = d3.scale.linear().domain([ymin, ymax])
.range([0 + margin_y, h - margin_y]);
}
};
因为获得的新数据必须重新绘制,所以您需要删除无效的 SVG 元素并用新的元素替换它们。让我们对刻度标签和数据行都这样做(见清单 26-10)。最后,需要定时重复刷新图表。因此,使用requestAnimFrame()
函数,您可以重复执行UpdateData()
函数的内容。
清单 26-10。ch26_01.html
function updateData() {
...
if(newlast < ymin){
ymax = ymax - (ymin - newlast);
ymin = newlast;
y = d3.scale.linear().domain([ymin, ymax]).range([0 + margin_y, h - margin_y]);
}
var svg = d3.select("body").transition();
g.selectAll(".yLabel").remove();
g.selectAll(".yLabel")
.data(y.ticks(5))
.enter().append("svg:text")
.attr("class", "yLabel")
.text(String)
.attr("x", 25)
.attr("y", function(d) { return -y(d) })
.attr("text-anchor", "end");
g.selectAll(".line").remove();
g.append("svg:path")
.attr("class","line")
.attr("d", line(data));
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000);
};
})();
requestAnimFrame(setTimeout(updateData,3000));
render();
};
现在,当点击 Update 按钮时,从左边开始有一条线画出由生成随机变量的函数获得的值。一旦该线到达图表的右端,它将更新每次采集,仅显示最后十个值(见图 26-1 ) .
图 26-1。
A real-time chart with a start button
使用 PHP 从 MySQL 表中提取数据
最后,是时候使用数据库中包含的数据了,这种场景更符合您的日常需求。您选择 MySQL 作为数据库,并使用超文本预处理器(PHP)语言查询数据库,获得 JavaScript 对象表示法(JSON)格式的数据,以便 D3 可以读取。你会发现,一旦用 D3 构建了一个图表,过渡到这个阶段就很容易了。
下面这个例子不是为了解释 PHP 语言或者其他任何语言的使用,而是为了说明一个典型的真实案例。这个例子显示了将你所学的知识与其他编程语言结合起来是多么简单。通常,像 Java 和 PHP 这样的语言提供了一个很好的接口来从它们的来源(在这个例子中是数据库)收集和准备数据。
从 TSV 的档案开始
为了更清楚地理解你已经知道的东西与 PHP 和数据库接口之间的转换,让我们从一个你应该熟悉的案例开始(见清单 26-11)。首先,用这些系列数据编写一个制表符分隔的值(TSV)文件,并将它们保存为data_13.tsv
。
清单 26-11。data_13.tsv
day income expense
2012-02-12 52 40
2012-02-27 56 35
2012-03-02 31 45
2012-03-14 33 44
2012-03-30 44 54
2012-04-07 50 34
2012-04-18 65 36
2012-05-02 56 40
2012-05-19 41 56
2012-05-28 45 32
2012-06-03 54 44
2012-06-18 43 46
2012-06-29 39 52
Note
注意,TSV 文件中的值是用制表符分隔的,所以当你编写或复制清单 26-11 时,记得检查每个值之间只有一个制表符。
实际上,正如所暗示的,你已经看到了这些数据,尽管形式略有不同;这些是data_03.tsv
文件中的相同数据(参见第二十章中的清单 20-60)。您将列date
更改为day
,并修改了日期的格式。现在,您必须添加清单 26-12 中的 JavaScript 代码,这将允许您将这些数据表示为多系列折线图。(该代码与第二十章中的“差异折线图”一节中使用的代码非常相似;关于清单 26-12 内容的解释和细节,请参阅该部分。)
清单 26-12。ch26_02.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="
http://d3js.org/d3.v3.js
<style>
body {
....font: 10px verdana;
}
.axis path,
.axis line {
fill: none;
stroke: #333;
}
.grid .tick {
stroke: lightgrey;
opacity: 0.7;
}
.grid path {
stroke-width: 0;
}
.line {
fill: none;
stroke: darkgreen;
stroke-width: 2.5px;
}
.line2 {
fill: none;
stroke: darkred;
stroke-width: 2.5px;
}
</style>
</head>
<body>
<script type="text/javascript">
var margin = {top: 70, right: 20, bottom: 30, left: 50},
w = 400 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
var parseDate = d3.time.format("%Y-%m-%d").parse;
var x = d3.time.scale().range([0, w]);
var y = d3.scale.linear().range([h, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5);
var xGrid = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5)
.tickSize(-h, 0, 0)
.tickFormat("");
var yGrid = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.tickSize(-w, 0, 0)
.tickFormat("");
var svg = d3.select("body").append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var line = d3.svg.area()
.interpolate("basis")
.x(function(d) { return x(d.day); })
.y(function(d) { return y(d["income"]); });
var line2 = d3.svg.area()
.interpolate("basis")
.x(function(d) { return x(d.day); })
.y(function(d) { return y(d["expense"]); });
d3.tsv("data_13.tsv", function(error, data) {
data.forEach(function(d) {
d.day = parseDate(d.day);
d.income = +d.income;
d.expense = +d.expense;
});
x.domain(d3.extent(data, function(d) { return d.day; }));
y.domain([
d3.min(data, function(d) { return Math.min(d.income, d.expense); }),
d3.max(data, function(d) { return Math.max(d.income, d.expense); })
]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "grid")
.attr("transform", "translate(0," + h + ")")
.call(xGrid);
svg.append("g")
.attr("class", "grid")
.call(yGrid);
svg.datum(data);
svg.append("path")
.attr("class", "line")
.attr("d", line);
svg.append("path")
.attr("class", "line2")
.attr("d", line2);
});
var labels = svg.append("g")
.attr("class","labels");
labels.append("text")
.attr("transform", "translate(0," + h + ")")
.attr("x", (w-margin.right))
.attr("dx", "-1.0em")
.attr("dy", "2.0em")
.text("[Months]");
labels.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -40)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Millions ($)");
var title = svg.append("g")
.attr("class", "title")
title.append("text")
.attr("x", (w / 2))
.attr("y", -30 )
.attr("text-anchor", "middle")
.style("font-size", "22px")
.text("A Multiseries Line Chart");
</script>
</body>
</html>
有了这段代码,你就得到图 26-2 中的图表。
图 26-2。
A multiseries chart reading data from a TSV file
转到真正的案例
现在,让我们转到实际情况,您将处理数据库中的表。对于数据源,您在 MySQL 的测试数据库中选择一个名为 sales 的表。在用这个名称创建了一个表之后,可以用执行 SQL 序列的数据填充它(见清单 26-13)。
清单 26-13。销售. sql
insert into sales
values ('2012-02-12', 52, 40);
insert into sales
values ('2012-02-27', 56, 35);
insert into sales
values ('2012-03-02', 31, 45);
insert into sales
values ('2012-03-14', 33, 44);
insert into sales
values ('2012-03-30', 44, 54);
insert into sales
values ('2012-04-07', 50, 34);
insert into sales
values ('2012-04-18', 65, 36);
insert into sales
values ('2012-05-02', 56, 40);
insert into sales
values ('2012-05-19', 41, 56);
insert into sales
values ('2012-05-28', 45, 32);
insert into sales
values ('2012-06-03', 54, 44);
insert into sales
values ('2012-06-18', 43, 46);
insert into sales
values ('2012-06-29', 39, 52);
最好是,您应该在一个单独的文件中编写 PHP 脚本,并将其保存为myPHP.php
。这个文件的内容如清单 26-14 所示。
清单 26-14 . myPHP.php
<?php
$username = "dbuser";
$password = "dbuser";
$host = "localhost";
$database = "test";
$server = mysql_connect($host, $username, $password);
$connection = mysql_select_db($database, $server);
$myquery = "SELECT * FROM sales";
$query = mysql_query($myquery);
if ( ! $myquery ) {
echo mysql_error();
die;
}
$data = array();
for ($x = 0; $x < mysql_num_rows($query); $x++) {
$data[] = mysql_fetch_assoc($query);
}
echo json_encode($data);
mysql_close($server);
?>
通常,一个 PHP 脚本可以通过它在特殊的开始和结束处理指令:<?php
和?>
中的封装来识别。每当我们需要连接到数据库时,通常都会用到这段简短但功能强大且用途广泛的代码片段。让我们浏览一下,看看它做了什么。
在本例中,dbuser
被选为用户,dbuser
被选为密码,但是这些值将取决于您想要连接的数据库。这同样适用于数据库和主机名值。因此,为了连接到一个数据库,你必须首先定义一组识别变量,如清单 26-15 所示。
清单 26-15 . myPHP.php
$username = "homedbuser";
$password = "homedbuser";
$host = "localhost";
$database="homedb";
一旦定义了它们,PHP 就提供了一组已经实现的函数,在这些函数中,您只需将这些变量作为参数传递,就可以与数据库建立连接。在这个例子中,您需要调用mysql_connect()
和myqsl_select_db()
函数来创建一个与数据库的连接,而不需要定义任何其他东西(参见清单 26-16)。
清单 26-16 . myPHP.php
$server = mysql_connect($host, $username, $password);
$connection = mysql_select_db($database, $server);
即使对于输入 SQL 查询,PHP 也被证明是一个真正实用的工具。清单 26-17 是一个非常简单的例子,展示了如何使用 SQL 查询从数据库中检索数据。如果您不熟悉 SQL 语言,查询是针对特定数据库的声明性语句,目的是获取数据库中包含的所需数据。您可以很容易地识别一个查询,因为它由一个SELECT
语句和一个 FROM 语句组成,并且几乎总是在末尾有一个WHERE
语句。
Note
如果你没有 SQL 语言的经验,并且想在不安装数据库和其他任何东西的情况下做一些练习,我建议你访问 w3schools 网站的这个网页: www.w3schools.com/sql
。在本文中,您将找到关于这些命令的完整文档,其中包含许多示例,甚至能够通过嵌入 SQL 测试查询来查询该站点提供的数据库证据(参见“亲自尝试”一节)。
在这个简单的例子中,SELECT
语句后面跟有'*'
,这意味着您想要接收在FROM
语句(在这个例子中是sales
)中指定的表中包含的所有列中的数据。
清单 26-17 . myPHP.php
$myquery = "SELECT * FROM sales";
$query = mysql_query($myquery);
一旦你做了一个查询,你需要检查它是否成功,如果出现错误就处理它(见清单 26-18)。
清单 26-18 . myPHP.php
if ( ! $query ) {
echo mysql_error();
die;
}
如果查询成功,那么您需要处理查询返回的数据。你把这些值放在一个名为$data
的数组中,如清单 26-19 所示。这部分非常类似于 D3 的csv()
和tsv()
函数,只是它不是从文件中逐行读取,而是从数据库中检索的表中读取。mysql_num_rows()
函数给出了表中的行数,类似于在for()
循环中使用的 JavaScript 的 length()函数。mysql_fetch_assoc()
函数将从查询中检索到的数据逐行分配给数据数组。
清单 26-19 . myPHP.php
$data = array();
for ($x = 0; $x < mysql_num_rows($query); $x++) {
$data[] = mysql_fetch_assoc($query);
}
echo json_encode($data);
该脚本的关键是对 PHP json_encode()
方法的调用,该方法将数据格式转换成 JSON,然后使用 echo 返回数据,D3 将解析这些数据。最后,您必须关闭与服务器的连接,如清单 26-20 所示。
清单 26-20 . myPHP.php
mysql_close($server);
现在,您回到 JavaScript 代码,只修改了一行(是的,只有一行!)(参见清单 26-21)。用json()
函数替换tsv()
函数,直接传递 PHP 文件作为参数。
清单 26-21。ch26_02b.html
d3.json("myPHP.php", function(error, data) {
//d3.tsv("data_03.tsv", function(error, data) {
data.forEach(function(d) {
d.day = parseDate(d.day);
d.income = +d.income;
d.expense = +d.expense;
});
最终,你会得到同样的图表(见图 26-3 )。
图 26-3。
A multiseries chart obtaining data directly from a database
摘要
这最后一章通过考虑两种不同的情况而结束。在第一个例子中,您看到了如何创建一个 web 页面,在这个页面中可以表示您实时生成或获取的数据。在第二个例子中,您学习了如何使用 D3 库来读取数据库中包含的数据。
结论
有了这一章,你就到了这本书的结尾。我必须说,尽管涵盖了大量的主题,但我还想补充许多其他主题。我希望这本书能让你更好地理解数据可视化的世界,尤其是图表。我还希望这本书为您提供了良好的数据可视化基础知识,并证明它对您处理图表的所有场合都是有价值的帮助。
二十七、书中例子的指南
Abstract
本附录提供了如何使用 XAMPP 和 Aptana Studios 在您的 PC 上创建一个开发环境的指南,该环境将允许您开发、运行和修复书中给出的示例。
本附录提供了如何使用 XAMPP 和 Aptana Studios 在您的 PC 上创建一个开发环境的指南,该环境将允许您开发、运行和修复书中给出的示例。
安装 Web 服务器
如今,在互联网上,您可以很容易地找到免费的软件包,其中包含了为您的所有示例以及与 web 世界相关的所有内容建立测试环境所需的一切。
这些软件包最大限度地减少了需要安装的程序数量。更重要的是,它们可以通过一次安装获得。这些包通常由一个 Apache HTTP 服务器组成;一个 MySQL 数据库;以及编程语言 PHP、Perl 和 Python 的解释器。最完整的包是 XAMPP(可以在 Apache Friends 网站[www.apachefriends.org/en/index.html
]
]下载)。XAMPP 是完全免费的,它的主要特点是它是一个跨平台的软件包(Windows、Linux、Solaris、MacOS)。此外,XAMPP 还包括一个 Tomcat 应用服务器(用于编程语言 Java)和一个 FileZilla FTP 服务器(用于文件传输)。其他解决方案是特定于平台的,正如其名称的首字母所示:
WAMP
(Windows)MAMP
(MacOS)LAMP
(Linux)SAMP
(Solaris)FAMP
事实上,XAMPP 是一个缩写;其字母代表以下术语:
X
,为操作系统A
,对于 Apache,web 服务器M
,对于 MySQL,数据库管理系统P
,PHP、Perl 或 Python 的编程语言
因此,选择最适合您的平台的 web 服务器解决方案,并将其安装在您的 PC 上。
安装 Aptana Studio IDE
一旦安装了 Web 服务器,就需要安装一个集成开发环境(IDE ),这是开发 JavaScript 代码所需要的。在本附录中,您将安装 Aptana Studio 作为您的开发环境。
访问 Aptana 站点( www.aptana.com
),并单击 Aptana Studio 3 软件的产品选项卡(在撰写本文时,最新版本是 3.4.2)。下载单机版(已经集成了 Eclipse IDE):Aptana_Studio_3_Setup_3.4.2.exe
。
下载完成后,启动可执行文件来安装 Aptana Studio IDE。在安装结束时,在启动应用时,您应该看到工作台打开,如图 27-1 所示。
图 27-1。
The Aptana Studio IDE workbench
在 Aptana Studio 的安装过程中,软件会检测各种浏览器和安装的 web 服务器,并相应地进行自我配置。
设置 Aptana Studio 工作空间
在开始开发书中的例子之前,您必须创建一个工作空间。首先,您应该在 Aptana Studio 上设置工作区,Web 服务器文档根目录就在这里。
这些是 XAMPP 的典型道路:
- 视窗:
C:\xampp\htdocs
- Linux:??]
- MAC OS:??]
而 WAMP 的情况是这样的:
C:\WAMP\www
因此,选择文件➤切换工作区➤其他。。。从菜单上。然后,在字段中插入 web 服务器文档根目录的路径,如图 27-2 所示。
图 27-2。
Setting the workspace on the document root
创建项目
创建工作空间的下一步是在 Aptana Studio 中创建一个项目:
图 27-3。
Creating a default project Select New ➤ Web Project from the menu. A window such as that shown in Figure 27-3 appears. Select Default Project, and click Next. Insert “charts” as the name of the project. This will be the directory in the workspace in which you will write all the example files described in the book, using Aptana Studio.
完成工作空间
一旦您设置了 Aptana Studio 工作区并创建了一个项目,您就完成了工作区。
让我们打开文档根目录并创建一个新目录,命名为src
。现在,wyou 将在整本书中使用的工作空间由两个目录组成:
src
charts
src
目录应该包含所有与库相关的文件。
charts
目录应该包含所有与书中例子相关的 HTML、图片和层叠样式表(CSS)文件(实际上是一个项目)。每个示例文件都应该在这个目录中创建(如果您喜欢以不同的方式做事情,这很好,但是为了包含库文件和图像,注意 HTML 页面中不同的路径引用是很重要的)。
Note
这本书附带的源代码(可以从 Apress 网站[ www.apress.com
]的源代码/下载区获得)实际上已经打包在一个工作区中了。有了它,你会发现 charts 项目的两个版本:内容交付网络(CDN)和本地。charts_CDN
目录包含所有引用从 CDN 服务远程分发的库的例子。charts_local
目录提供了所有引用在src
目录中找到的库的例子。
用库填充 src 目录
如果你选择了通过引用本地的库来开发 HTML 页面,那么就有必要下载它们所有的文件。这些文件将被收集在src
目录中。这是一个很好的方法,因为您可以开发几个使用相同库的项目,而不必为每个项目复制它们。
本附录中列出的版本是用于实现书中示例的版本。如果您安装其他版本,可能会出现不兼容的问题,或者您可能会观察到与描述不同的行为。
jqPlot 库版本 1.0.8(包括 jQuery 库版本 1.9.1)
Visit the jqPlot web site ( https://bitbucket.org/cleonello/jqplot/downloads/
), and download the compressed file (.zip, .tar.gz or tar.bz2) for the library: jquery.jqplot.1.0.8r1250
. Extract all content. You should get a directory named dist
, containing the following subdirectories and files:
doc
examples
plugins
- 一系列文件(
jquery.min.js
、jquery.jqplot.min.js
等)
Copy the set of files and the plugins
directory, and place in src
.
jquery UI 库版本 1.10.3,带有平滑主题
Visit the JQuery user interface library (jQuery UI) site ( http://jqueryui.com/themeroller/
), and download the library from ThemeRoller, with the smoothness theme: jquery-ui-1.10.3.custom.zip
. Extract all content. You should get a directory named jquery-ui-1.10.3.custom
, with the following directories inside:
css
js
development-bundle
Copy the css
and js
directories, and place in src
.
D3 库版本 3
Visit the D3 site ( http://d3js.org
), and download the library: d3.v3.zip
. Extract all content directly, and place in the src
directory. Now, you should have two new files in the src
directory:
d3.v3.js
d3.v3.min.js
Highcharts 库版本 3.0.5
Visit the Highcharts site ( www.highcharts.com
), and download the library: Highcharts-3.0.5.zip
. Extract all content. You get a directory with several directories inside. Copy only the js
directory, and place in src
.
这样你就获得了src
目录,其中应该包含如图 27-4 所示的子目录和文件。
图 27-4。
The files and subdirectories contained in the src
directory Note
按照惯例,您正在开发charts
目录中的示例。如果您想这样做,当您将其他文件包含在网页中时,您需要考虑新的路径。
如果您正在开发charts
目录中的 HTML 页面,您需要使用以下代码:
<script type="text/javascript" src="../src/jquery.min.js"></script>
相反,如果您喜欢直接开发它,在文档根目录中,您可以使用:
<script type="text/javascript" src="src/jquery.min.js"></script>
简而言之,根据您实现的页面,考虑您要包含的文件的路径是很重要的。
运行示例
在工作区中创建或复制 HTML 文件后,要在 Aptana Studio IDE 中运行该文件,请从菜单中选择运行➤运行,或单击工具栏上的运行按钮(参见图 27-5 )。
图 27-5。
The Run button from the toolbar
您的默认浏览器将立即打开,并加载选定的 HTML 页面。
查看运行配置(参见图 27-6 ),选择运行配置。。。从运行图标的上下文菜单中。例如,让我们将http://localhost/
设置为您的基本 URL 为此,请选择“追加项目名称”选项,如图所示。然后,单击“应用”按钮确认您的设置。
图 27-6。
The run configuration for each browser must be set correctly
现在,您已经具备了轻松完成书中所有示例所需的一切。
一旦您对 Aptana IDE 有了一定的了解,您会发现它是一个开发许多其他项目的优秀环境,无论是用 JavaScript 还是用其他编程语言(例如 PHP)。
现在,玩得开心点!
摘要
本附录提供了如何使用 XAMPP 和 Aptana Studios 在您的电脑上创建开发环境的指南。使用这些应用的选择不是强制性的,许多其他解决方案也是可能的;互联网上有许多可用于执行类似操作的应用。但是,如果您希望实现并快速测试书中描述的示例,这个环境将是一个不错的选择。
二十八、jqPlot 插件
Abstract
本附录显示了 jqPlot 发行版中可用插件的完整列表(见表 B-1)。并非所有这些插件都在本书中讨论过;更多信息请访问 jqPlot 网站( www.jqplot.com
)。
本附录显示了 jqPlot 发行版中可用插件的完整列表(见表 B-1 )。并非所有这些插件都在本书中讨论过;更多信息请访问 jqPlot 网站( www.jqplot.com
)。
表 B-1。
Available Plug-ins in the jqPlot Distribution (version 1.0.8)
| 名字 | 类型 | 描述 | | --- | --- | --- | | jqlot,barrenderer|渲染器|画一个条形图。||jqlot,barrenderer|渲染器|画一个条形图。||.jqplot.BezierCurveRenderer | 渲染器 | 将线条绘制成堆叠的贝塞尔曲线。 | | .jqplot.blockrenderer|渲染器|画一个xy方块图。块图中的数据点显示为彩色方块,内部带有文本标签。||.jqplot.blockrenderer|渲染器|画一个xy方块图。块图中的数据点显示为彩色方块,内部带有文本标签。|| . jqplot . bubblerenderer | 渲染器 | 画一个气泡图。气泡图的数据点显示为彩色圆圈,内部带有可选的文本标签。 | | .jqplot.canvasaxilabelrenderer|渲染器|绘制轴标签,用一个‘canvas‘元素来支持高级特性,比如旋转文本。这个渲染器使用一个单独的渲染引擎在画布上绘制文本。|| . jqplot . canvasaxistickrenderer | 渲染器 | 用一个`canvas`元素绘制轴记号,以支持高级功能,如旋转文本。这个渲染器使用一个单独的渲染引擎在画布上绘制文本。 | | .jqplot.canvasoverlay重叠|插件|在图表上画线。||.jqplot.CanvasTextRenderer | 渲染器 | `canvastext.js`插件的修改版,由 Jim Studt ( [`http://jim.studt.net/canvastext/`](http://jim.studt.net/canvastext/) )编写。 | | .jqplot.categoryaxisrenderer|渲染器|呈现类别样式轴,系列的y数据值之间的像素间距相等。||.jqplot.ciParser | 插件 | 将自定义 JavaScript 对象符号(JSON)数据对象转换为 jqPlot 数据格式的函数。 | | .jqplot.游标|插件|代表光标的类,如图上所示。||jqplot.dateaxisrenderer|渲染器|将轴呈现为一系列日期值。|| . jqplot . donutrenderer | 渲染器 | 画一个圆环图;x 值(如果存在)用作切片标签,y 值给出切片大小。 | | .jqplot.Dragable|插件|制作用户可以拖动的标绘点。|| jqp lot .增强传奇人物 | 渲染器 | 绘制具有高级功能的图例。 | | .jqplot.漏斗渲染器|渲染器|画一个漏斗图;x值(如果有)用作标注,y值给出面积大小。漏斗图仅绘制单一系列。|| jqlot,高个子 | 插件 | 当鼠标悬停在数据点上时高亮显示它们。 | | jqplot . json 2 | 插件 | 创建一个包含两个方法的 JSON 对象:`stringify()`和`parse()`。 | | .jqplot.logaxisrenderer|渲染器|渲染对数轴。|| . jqp lot . meko ximatics 渲染器 | 渲染器 | 与 MekkoRenderer 插件一起使用;将 y 轴显示为从 0 到 1 (0 到 100%)的范围,将 x 轴显示为每个系列的刻度,缩放到所有 y 值的总和。 | | jqpsk.MEKkorender|渲染器|绘制Mekko样式的图表,在二维图形上显示三维数据。||.jqplot.MeterGaugeRenderer | 渲染器 | 绘制仪表图。 | | jqlot,移动|插件|jQuery移动虚拟事件支持。|| jqpplot .奥克雷德尔 | 渲染器 | 绘制开盘-盘高-盘低-收盘图、蜡烛图和盘高-盘低-收盘图。 | | jqplot.复制|渲染器|画一个饼状图;x值(如果存在)用作切片标签,y值给出切片大小。|| . jqplot .点标签 | 插件 | 在数据点放置标签。 | | .jqplot.pyramidaxisrenderer|渲染器|与PyramidRenderer插件一起使用;在底部显示两个x轴,在中心显示y轴。||.jqplot.PyramidGridRenderer | 渲染器 | 与 PyramidRenderer 插件一起使用;在`canvas`元素上创建网格。 | | .jqlot.pyramids渲染器|渲染器|画一个金字塔图。|| jqp lot .趋势线 | 插件 | 自动计算并绘制绘制数据的趋势线。 |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
2023-08-19 赏味不足:详细来聊下轻资产运作,我从不做重资产
2023-08-19 老隋:什么赚钱就做什么,记住轻资产运营,试错成本低
2023-08-19 iBooker 技术评论 20230819:打工是风险最高的事情
2023-08-19 卓钥商学苑:创业期间被合伙人背叛了怎么办?处理方式有哪些?