首页 前端知识 HTML5 和 JavaScript 社交数据可视化(一)

HTML5 和 JavaScript 社交数据可视化(一)

2024-08-07 00:08:26 前端知识 前端哥 630 851 我要收藏

原文:zh.annas-archive.org/md5/C48D9EE20D427590BDD70160A9A344F0

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

世界是一个有趣的地方,对此毫无疑问。我们通过感官体验世界,这些感官收集数据供我们的大脑处理。世界经常是混乱的,我们需要深入思考才能理解事物。为了简化这个过程,数据可以转换成更容易理解的其他形式。

本书是关于将数据塑造成更易理解的形式。它是关于利用我们这个时代最丰富的数据源——社交网络,并将它们的大量数据转换成可理解的形式。为此,我们使用了最新的 HTML 和 JavaScript。

本书涵盖内容

第一章,数据可视化,向我们介绍了一个充满不断增长数据集的世界。它还讨论了如何使用可视化作为我们的独木舟来驾驭这条数据之河。

第二章,JavaScript 和 HTML5 用于可视化,探讨了 HTML 和 JavaScript 中的新特性,这些特性为数据可视化提供了机会。它讨论了画布和可缩放矢量图形。

第三章,OAuth,探讨了常常令人困惑的 OAuth 技术,并展示了如何将其用于将权限委托给我们的应用程序。这是因为社交网站上的大量数据是私有的,我们可能需要这些数据。

第四章,JavaScript 用于可视化,介绍了 Raphaël.js 和 d3.js,这两个都是非常出色的 JavaScript 库,可以减少手动构建可视化的痛苦,否则这是一个耗时且容易出错的任务。

第五章,Twitter,探讨了如何从 Twitter 检索数据并使用它来构建可视化。

第六章,Stack Overflow,探讨了如何检索流行的 Stack Overflow 的数据 API,它提供了一些令人着迷的可视化机会,可以用来创建交互式图表。

第七章,Facebook,探讨了 Facebook JavaScript API 以及如何使用它来检索数据作为我们下一个可视化的基础。在我心中,原始的、仍然是世界上最大的社交媒体网络就是 Facebook。

第八章,Google+,探讨了谷歌最新的社交媒体尝试以及如何检索数据来创建力导向图。

本书所需准备

使用这本书中的示例和代码所需的工具非常少。你需要安装 node.js (node.org),这在 第五章 Twitter 中有所介绍。你想要下载 d3.js (d3js.org),jQuery (jquery.com),和 Raphael.js (raphaeljs.com/)。所有的演示可以在任何现代的网页浏览器中查看。代码已经针对 Chrome 进行测试,但应该在 FireFox、Opera 上也能工作,甚至包括 Internet Explorer。

这本书适合谁

这本书适合所有对数据感到兴奋并希望与他人分享这种兴奋的人。任何对可以从社交网络提取的数据感兴趣的人也会发现这本书很有趣。读者应该具备 JavaScript 和 HTML 的基本工作知识。jQuery 在整本书中都被多次使用,所以读者最好熟悉这个库的基本知识。对 node.js 有一些了解会有帮助,但不是必需的。

约定

在这本书中,你会发现有许多种文本样式来区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词汇如下所示:“一旦权限被分配,Facebook 会将用户重定向回你的 redirect_uri,允许你利用令牌来查询 Facebook API。”

代码块如下所示:

OAuth.initialize('<Your Public key>');
OAuth.redirect('facebook', "callback/url");

当我们希望吸引你的注意力到代码块的某个特定部分时,相关的行或项目会被设置为粗体:

function onSignInCallback(authResult) {
      gapi.client.load('plus','v1', function(){
        if (authResult['access_token']) {
          $('#gConnect').hide();
                           retrieveFriends();
        } else if (authResult['error']) {
          console.log('There was an error: ' + authResult['error']);
          $('#gConnect').show();
        }
        console.log('authResult', authResult);
      });
    }

新术语重要词汇以粗体显示。例如,您在屏幕上、菜单或对话框中看到的词汇,在文本中如下所示:“这可以通过点击 API Access 选项卡中的 Create an OAuth 2.0 client ID… 来完成。”

注意

警告或重要说明以这样的盒子形式出现。

技巧

技巧和窍门就像这样出现。

读者反馈

我们对读者的反馈总是开放的。让我们知道你对这本书的看法——你喜欢什么或者可能不喜欢什么。读者反馈对我们开发您真正能从中获得最多收益的标题非常重要。

要发送给我们一般性反馈,只需发送一封电子邮件到 <feedback@packtpub.com>,并通过消息主题提及书名。

如果你在某个话题上有专业知识,并且有兴趣撰写或贡献一本书,请查看我们在 www.packtpub.com/authors 上的作者指南。

客户支持

现在你拥有了一本 Packt 出版的书籍,我们有许多资源可以帮助你充分利用你的购买。

下载示例代码

您可以从您在www.packtpub.com的账户上下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

错误

虽然我们已经竭尽全力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——我们将非常感谢您能向我们报告。这样做可以节省其他读者的挫折感,并帮助我们改进本书的后续版本。如果您发现任何错误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击错误提交****表单链接,并输入您的错误详情。一旦您的错误得到验证,您的提交将被接受,错误将会上传到我们的网站,或添加到该标题下的现有错误列表中。您可以通过从www.packtpub.com/support选择您的标题来查看任何现有的错误。

盗版

互联网上的版权材料盗版是一个持续存在的问题,涵盖所有媒体。 Packt 对此非常重视,如果您在互联网上以任何形式发现我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

如果您发现有疑似侵犯版权的材料,请通过<copyright@packtpub.com>联系我们。

我们感谢您在保护我们的作者和为我们提供有价值内容方面所提供的帮助。

问题

如果您在阅读本书的过程中遇到任何问题,可以通过<questions@packtpub.com>联系我们,我们将尽最大努力解决问题。

第一章:可视化数据

就在几年前,这本书还不可能完成。社交媒体、数据处理和网络技术的快速发展实现了一个不同领域的融合。从这个融合中,我们可以创造出关于奇异话题的数据迷人展示。数据中继承的美可以通过一种大众可以接触的方式暴露出来。如下面的词图可视化(gigaom.com/2013/07/19/the-week-in-big-data-on-twitter-visualized/), 可以在解锁隐藏信息的同时,用非凡的体验取悦用户:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这幅可视化中单词的大小给出了它们使用频率的线索。单词的位置是通过设计创造愉悦可视化的算法计算得出的。

在本章中,我们将探讨数据增长如此之大,以至于我们需要改变观察它的工具。

外面有很多数据

对任何人来说,人类记录的数据量以惊人的速度增长这一点都不应该感到惊讶。每隔几年,数据存储公司 EMC 就会发布一份关于保存了多少数据的报告(www.emc.com/collateral/analyst-reports/idc-the-digital-universe-in-2020.pdf)。2012 年,估计从 2005 年到 2020 年,全球存储的数据量将从 130 艾字节增长到 40,000 艾字节。这意味着地球上每个人的数据量为 5.2 太字节。这是一个令人震惊的信息量,了解其中有多少信息存在是困难的。到 2020 年,这相当于每个人 11 个 DVD 的容量。如果我们改用容量为 50GB 的蓝光盘,堆叠起来存储全部 40,000 艾字节的数据,仍然会超出月球轨道。

随着人们将更多的生活内容搬到线上,数据的增长是不可避免的。智能手机的普及使每个人都成为了摄影师。Instagram,一个流行的图片分享网站,每天收集大约 4000 万张照片。人们不禁想知道世界上真的需要多少张人们的餐食照片。在过去的几个月里,视频片段分享网站如 Vine 和 Instagram 的数量激增,产生了大量的数据。为了扩大智能手机在收集摄影数据方面的应用,正在创建大量的新设备。最新一代的智能手机除了常见的 GPS、陀螺仪、地磁和加速度传感器外,还包括温度、湿度和压力传感器。这些使得能够记录用户周围世界的准确表示。

传感器数量的增加并不仅仅局限于智能手机领域。传感器和无线电的价格已经达到一个临界点,使得创建能够记录并传输关于世界数据的独立设备变得经济可行。曾经有一段时间,构建一个能够向中心设备报告温度的传感器阵列,只是大型 SCADA 系统(监控和控制应用程序)的领域。我第一份工作之一就是在一个炼油厂测试一系列 IP 兼容的监控设备。当时,网络硬件本身的成本就高达数百万美元。而现在,同样的系统可以用几百美元就搭建起来。访问如 Kickstarter 或 Indiegogo 等众包网站,你会发现无数蓝牙或 Wi-Fi 功能的传感器设备。这些设备可能会帮你找到丢失的钥匙,告诉你给番茄浇水的时间。大量这样的设备存在,表明我们正进入一个自主设备报告世界的时代。一种物联网正在兴起。

与此同时,存储数据的每千兆字节的成本正在下降。更便宜的存储使得跟踪之前可能会被丢弃的数据变得经济可行。20 世纪 70 年代,英国广播公司(BBC)有一个政策,一旦电视节目达到一定年龄,就销毁其录像。这导致经典电视剧《神秘博士》(Doctor Who)超过一百集的丢失。20 世纪 60 年代存储介质的低数据密度意味着保留完整的档案是成本高昂的。现在这样的删除是不可想象的,因为存储视频的成本已经大幅下降。在亚马逊服务器上存储一 GB 信息的成本大约是一个月一角钱,如果内部有合适的专业知识,成本甚至可能更低。帕金森定律陈述如下:

工作会扩展以填满完成它所需的时间。

在这个定律的重新表述中,对我们来说,“数据量将增长到填满可用空间。”

数据的增长使我们的生活变得更加困难。虽然数据量一直在增长,但我们理解它们的能力大体上停滞不前。处理和精炼大量数据可用的工具并没有跟上步伐。对数 GB 数据运行简单的查询是一个耗时的过程。比如“列出所有包含单词’百事’的推文”这样的查询,除了并行工作的机器集群,实际上无法在任何其他设备上完成。即使结果返回,匹配记录的数量也太大了,无法由一个人甚至一个团队的人处理。

术语“大数据”通常用来描述越来越常见的非常大的数据集。像大多数已经成为市场营销术语的术语一样,大数据由不同的人和公司定义不同。在这本书中,我们认为大数据是指任何数量,在消费者级别的硬件上使用传统数据库工具运行简单查询因计算、存储或检索限制而变得困难。

理解大数据的世界是一个复杂的提议。以有意义的方式可视化数据将是未来十年面临的巨大问题之一。更重要的是,这个问题需要在传统上数据并不丰富的领域得到解决。

考虑一个咖啡店;这不是一个会产生大量数据的公司。然而,渴望数据的消费者开始要求知道他们最爱的咖啡豆来自哪里,烘焙了多久,以及是如何冲泡的。一个名为ThisFish的类似计划已经存在,允许消费者追踪他们海鲜的来源(thisfish.info)一直追溯到捕获时。以一种易于访问的形式提供关于咖啡的数据成为咖啡店的卖点。下面的屏幕截图显示了一个咖啡店典型的标签,显示了豆子的来源、烘焙时间和有机认证:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

人们对数据非常感兴趣,尤其是关于他们习惯的数据。但是,尽管人们对数据很感兴趣,没有人愿意浏览 Excel 文件。他们希望以一种易于访问和有趣的方式展示数据。

对数据感到兴奋

事实是数据很有趣!它令人惊叹地有趣,因为它讲述了一个故事。问题是,大多数时候这个故事都隐藏在一堆看似无趣的数字后面。提取关键数据并将其以有意义的方式展示给人们需要一些技巧。人类是视觉生物,相比于数字表格,他们更能轻易地处理图像。

最佳的数据可视化源于对可视化主题的激情。难道我们不是在工作中如果主题是我们真正感兴趣的事情时会做得更好吗?伟大的可视化不仅仅是教育他们的观众,它们还让用户感到愉悦。它们以一种新颖的方式呈现数据,仍然容易被观众理解。伟大的可视化剥离了多余的信息,揭示了一个信息核心。与此同时,伟大的可视化具有一定的美感。不要被误导,认为这种美感没有目的。在一个注意力持续时间越来越短的世界里,仍然有美丽的位置。当以审美上令人愉悦的可视化呈现时,我们仍然会停下来片刻。美丽所购买的额外几秒钟可能正是让人们感兴趣并吸收你的意思的时间。

即使是最无害的数据也有值得讲述的故事。对大多数人来说,税收收入统计数据似乎是最不有趣的。然而,在那大量数据中找到了一些非常吸引人的故事。数据讲述了一些公司避免支付税收的故事。它还讲述了哪些城市的人均收入最高的故事。在这些无聊的数据中,可以通过热情地应用数据可视化提取出无数有趣的的故事。

数据有很多种含义,但它永远不会无聊。你也可以对数据感到兴奋,并发现任何数据集中隐藏的故事。在每一个数据集中,都有一个有趣结论等待像你这样的数据侦探揭露。你应该以数据可视化的形式与他人分享你的兴奋。

Excel 之外的数据

到目前为止,世界上最受欢迎的数据操作和可视化工具是微软的 Excel。Excel 已经存在了近三十年,在此期间,它已经发展成为企业进行数据分析事实上的工具。Excel 具备对数据进行排序和分组的能力,并能为结果信息创建图表。

正如我们之前所看到的,世界上的数据量是巨大的。大多数数据可视化的第一步通常是筛选和汇总数据,将其缩小到包含您想与用户分享的关键洞察的数据集。这听起来像是提取,意味着这是一个有观点的过程,这是因为它确实如此。呈现一个无偏见的可视化几乎是不可能的。不过,这也没关系。并不是每个人都精通您的数据,引导他人得出您的结论是有价值的。

你会发现,你从哪里获得的数据,从中提取可视化的数据几乎从来不是你直接可以使用的格式。你需要操纵数据,使其达到你可以使用的形式。如果你的源数据集足够小,你的操作足够简单,你也许可以在 Microsoft Excel 中进行预处理。Excel 提供了一系列用于排序、筛选和总结数据的工具。关于如何在 Excel 中处理数据以及如何创建图表的书籍和文章有很多,但我们在这里不会深入探讨。

Excel 的问题是它已经过时了。每个人都见过 Excel 出来的那些相当平淡的图表。除了一两个特例,这些图表和 Excel 95 产生的图表是一样的。数据的兴奋在哪里?似乎 missing 了。如果你完全在 Excel 中创建你的可视化,你的用户将错过你对数据的热爱。

瑞士军刀因其拥有十几种不同的功能而闻名。你可以用同样的工具打开一瓶葡萄酒,也可以用它去除马蹄上的石头(这在大多数地方都是一个更常见的用途)。当你制造一个多功能工具时,你最终会得到一个什么都不擅长的工具。仅仅看看微软 Excel 的帮助索引的长度就应该告诉你,Excel 稳稳地落在多功能工具的范畴里。你可以用 Excel 做会计,或者跟踪你跑 5 公里的速度;你甚至可以用那数据建立图表。但你能做的就是制作真正好的图表。为此,你需要一些有数据可视化更狭窄焦点的专用工具。

社交媒体数据

我们谈了很多关于可视化和数据,但这本书标题的另一个部分是关于社交媒体的。除非你一直住在没有互联网的山洞里,否则你对过去十年席卷全球的社交媒体浪潮至少会有一点了解。真的只是十年吗?Facebook 成立于 2004 年。虽然我们可以在 2004 年之前找到例子,但我认为 Facebook 是第一个进入大众意识的社交媒体网站。

定义一个网站究竟是什么使它成为一个社交媒体网站是困难的。网站上必须有一些社交互动的方面,以及用户之间某种形式的联系。为了避免将任何带有评论区的网站都称为社交媒体网站,该网站的主要目的是为了启用用户之间的互动。这些网站上的内容通常是用户生成的,而不是由网站所有者创建的。社交媒体网站使用户之间可以进行互动,有相似兴趣的用户可以交流。

我为什么要关心?

社会媒体在我们世界中扮演的角色不容小觑。即使你避免加入所有社交媒体网站,并认为社交媒体对你的生活没有影响,它实际上确实有。社交媒体在现实世界中的影响的很好例子就是新闻媒体对社会媒体的依赖程度。今年早些时候,美联社的推特账户被黑客攻击,发送了几条消息,暗示白宫遭到恐怖分子袭击。虽然这一消息很快被驳斥,但股市在消息传出后大幅下跌。如果这种欺诈行为没有被迅速发现,真实世界的后果可能会更糟。

社交媒体的数据为世界各地发生的事件提供了背景。只需查看 Twitter 上的趋势,就可以挑选出当天的重大新闻故事。随着报纸订阅量的下降,Twitter、Facebook 和其他网站上的人数增长。传统新闻机构已经开始在它们的故事中整合通过社交媒体的评论和分享功能。故事下的评论往往成为故事本身,而不仅仅是提供一个元故事。许多评论员指出,Twitter 在阿拉伯之春甚至在美 国抗议中的重要性。社交媒体正在迅速超越更传统的新闻来源,成为社会发展的驱动力。

社交媒体不仅限于人与人之间的互动。越来越多的企业使用社交媒体与客户建立联系。通常,从一家无脸的公司获得服务最好的方式是在他们的 Facebook 页面上发消息或给他们发推文。我确实有过这样的经历, tweet 关于一家公司或一项服务,然后他们的社交媒体人员会联系我。任何赋予公司开发更好客户关系权力的工具都可能拥有长久的生命力。

从可视化的角度来看,社交媒体是有趣数据的一个巨大来源。我想象不出有什么数据源比人与人之间的社交互动更有吸引力。人类进化成社交动物,所以我们天生对发生在我们的社交圈内的事情感兴趣。除了它们的网站,许多社交媒体网站还有 API,促进构建使用它们数据的应用程序。理论上是,如果它们能为它们有价值的数据启用一个生态系统,人们会更频繁地访问它们的网站,第三方应用程序甚至可能会吸引新用户。

社交媒体是大数据的定义。Facebook 有大约十亿用户,每个用户可能每天产生十几条数据。Twitter、LinkedIn 和 Facebook 在发现他们需要处理的数据量太大,超出了传统数据库的承载能力后,都创建了自己的数据库技术。幸运的是,很少需要处理社交媒体的全部数据。通过各种数据访问 API,可以访问更窄的数据集。关键是尽可能将筛选和聚合工作转移到社交媒体网站上。通过探索可用的信息,可以得出有趣的结论,并通过可视化手段揭示用户通常不易察觉的信息。

HTML 可视化

这个拼图的最后一片是 HTML5。当我年轻的时候,HTML 的新版本意味着来自万维网联盟的另一个冗长的规格说明。HTML 新版本的规格过程需要数年时间,由来自微软和 IBM 等大型技术组织的成员组成的委员会来规划。虽然有一个 HTML5 的规格说明,但它没有以前版本那么正式。HTML5 这个术语已经用来描述可以用来创建强大网络应用程序的一系列面向未来的技术。

HTML5 包括以下多样化的功能的规格说明:

  • 网络工作者(多线程 JavaScript)

  • 触摸屏设备的触摸事件

  • 微数据格式

  • 画布

  • 可缩放矢量图形

  • 摄像头 API

  • 地理定位 API

  • 离线数据

通过这些新的 API 和功能,HTML5 不仅在浏览器上,而且在移动设备和桌面上都成为了一个重要的角色。通过像 PhoneGap(phonegap.com/)这样的工具包,HTML 和微软的 WinJS JavaScript 可以作为 iPhone、Android、Windows Phone 甚至黑莓手机的主要开发语言。原生 API 被绑定到 JavaScript 等效物上,使得 JavaScript 应用程序能够打开摄像头、GPS 和文件系统。HTML5 还可以用作 Windows 8 风格应用程序(以前称为 Metro)的开发平台。在非 Windows 平台上,可以使用类似于 Adobe Air(www.adobe.com/products/air.html)的工具包来用 HTML5 开发桌面应用程序。HTML5 提供了一个多平台开发环境,允许将 Web 技能带到平板电脑和桌面上。

离线数据工具消除了对 web 服务器来提供内容的依赖。直接在客户端机器上嵌入数据,而不是反复从服务器拉取,使得应用程序能够真正地移动化——网络变得不再至关重要。

HTML5 对可视化开发者来说非常有益。Canvas 和 SVG 都提供了诱人的功能。CSS3 也允许在样式上具有更大的灵活性。在 HTML5 出现之前,要在浏览器中实现交互式数据可视化,最好的方法是使用 Java Applets 或 Adobe Flash 等第三方工具。尽管这些技术的采用率很高,但仍然排除了一大批用户。即使采用率高,这些工具在野外的版本也往往是过时的。Java Applets 或 Adobe Flash 都不支持越来越受欢迎的移动平台。另一方面,HTML5 在大多数智能手机上以某种形式得到支持。

在 HTML 中开发可视化的最佳特性之一是,它允许用户与可视化互动。著名的可视化,如 伦敦地铁地图,因为画在静态的纸上而变得残废。交互提供了一个前所未有的用户参与度新层次。如果用户找到操纵可视化以得出全新结论的方法,你也不会感到惊讶。

业界对 HTML 和 JavaScript 技术的支持令人印象深刻。所有技术巨头都投入巨资开发基于 HTML5 和 JavaScript 的浏览器和发展工具。网页开发领域的变化速度令人惊叹。几乎每周我都会听说一个创新的新 JavaScript 库或一个新的开发平台。云托管的便捷性使得初创公司在网上蓬勃发展。

在选择一个用于开发可视化的工具时,HTML 是一个出色的选择。广泛的支持、良好的工具和众所周知的 API 确保了开发将是一种乐趣。好吧,也许不是乐趣,但至少相对无痛。HTML 和 JavaScript 是所有网页开发者的通用语言。无论开发是用 Ruby on Rails、ASP.NET 还是甚至是 Wordpress 作为后端,前端总是用 HTML 和 JavaScript 编写。这为选拔人才提供了一个大的人才库。

总结

向用户传达信息是一项棘手的工作。问题在于,现在可用的巨量数据使得这一问题变得更加复杂。作为一名可视化开发者,你的任务是从无关紧要的数据云中筛选出你感兴趣的部分,然后以一种有趣的方式将这些数据展示给用户。人们对数据感兴趣,但他们很少愿意花时间整理大量的表格数据。可视化通常是向用户展示这些数据的最佳工具。

社交媒体网站易于获取的高质量社交数据与新的可视化工具的结合,为我们创造有趣的可视化提供了前所未有的机会。通过能够超越标准微软 Excel 图表和表格的开发者们的热情,不仅有静态可视化的未来,还有交互式、有趣的可视化,这些可视化将在用户探索数据的 previously invisible aspects 时令他们感到愉悦。

在下一章中,我们将探讨一些使用现代网络开发工具创建可视化的方法。

第二章:JavaScript 和 HTML5 用于可视化

在上一章中,我提到了 HTML5 的一些发展使可视化变得更容易。这一章将探讨其中的一些,具体如下:

  • 画布(Canvas)

  • 可扩展矢量图形(SVG)

如果您熟悉这两个工具的功能,您可能想跳过这一章节。

画布

Canvas 是 HTML5 的一个功能,在技术文章和演示中经常被提及。Canvas 所能实现的功能非常令人印象深刻,因此它频繁出现并不令人意外。Canvas 提供了一个低级的位图接口用于绘图。您可以把它想象成浏览器中的 Microsoft Paint。画布上生成的所有图像都是光栅图像,这意味着图像是由像素网格而不是几何对象集合构建的,这与矢量图像的情况不同。与画布上绘制的元素交互必须通过过滤器和全局转换;精确控制如果可能的话会遇到问题。

在您的页面上创建一个画布元素很简单。您只需要添加一个如下所示的 HTML 元素:

<canvas width="200" height="200">Alternate text here</canvas>

提示

下载示例代码

您可以在 Packt 出版社网站上购买的图书的示例代码文件可以从您的账户www.packtpub.com下载。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您还可以在这本书的所有代码示例中找到github.com/stimms/SocialDataVisualizations

这将创建一个 200 x 200 像素的方形画布。如果使用的浏览器不支持画布元素,将显示替代文本。幸运的是,您看到这个警告的情况相当罕见,因为画布得到了广泛支持。到 2013 年中,87% 的互联网用户可以使用画布。要获取最新的数字,请查看caniuse.com/#feat=canvas。这个网站还提供了其他 HTML5 功能的浏览器支持信息。在 iOS、Android、BlackBerry 和 Windows Phone 上,移动浏览器也有支持。唯一不支持画布的常用浏览器是 Internet Explorer 的版本 prior to Version 9。

如果您处于目标受众广泛使用较旧的 Internet Explorer 版本的情况,不必过于担忧。有一个方便的 JavaScript polyfill;一段可下载的代码,为较旧的 Internet Explorer 版本提供画布功能。这个库可以在code.google.com/p/explorercanvas找到。要使用它,可以使用条件包含,如下面的代码片段所示:

<head>
<!--[if lt IE 9]><script src="img/excanvas.js"></script><![endif]--></head>

这只有在运行的浏览器是较旧的 Internet Explorer 版本时,才会包含 excanvas.js

Excanvas并不完全支持画布,缺少阴影和 3D 等特性,但大部分功能都是可用的。使用 JavaScript 版本的画布也会有性能损失,所以,如果你使用动画,它们可能不会像现代浏览器上那样流畅。这是为了触及剩下的一小部分用户而付出的小小代价。随着浏览器的更新,这个问题会变得越来越不重要。

在画布上绘制简单形状是容易的,但也有能力绘制一些非常复杂的对象,包括绘制 3D。我们将限制讨论一些更简单的形状和函数,这些函数在创建可视化时会有用。

在我们开始绘制之前,我们介绍一下画布的坐标系统。坐标系统的原点位于左上角,向右下方增长。要在画布上绘制,如以下所示,使用 JavaScript:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

绘制的第一步是获取一个画布实例的句柄,你想要在其中绘制。当然,你可以在页面上拥有尽可能多的画布元素。在这里,我们将创建一个 ID 为demo的画布:

<canvas height="200" width="200" id="demo"></canvas>

我们现在可以使用标准的 JavaScript 方法选择该元素:

var demoCanvas = document.getElementById("demo");

或者,如果你正在使用一个 CSS 选择器库,如 jQuery,你可以使用那个来选择元素:

var demoCanvas = $("#demo")[0];

下一步是获取对绘制上下文本身的引用。上下文包含了一组用于绘制的函数,我们将用它进行所有的画布操作:

var context = demoCanvas.getContext("2d");

画布支持多种基本形状,从中可以构建复杂的对象。最简单的形状就是矩形。可以通过调用strokeRect()函数来创建这个矩形:

context.strokeRect(20, 30, 100, 50);

这将创建一个从坐标(20,30)开始的矩形,宽度为 100 像素,高度为 50 像素。实际上,这将绘制一个之前显示的矩形,但向右移动了 20 像素,向下移动了 30 像素。所以,这个方法的签名是给出矩形的 x,y 起始坐标,然后是宽度和高度。除了strokeRect()函数外,还有fillRectclearRect。在画布上绘制时,可以画一个线条,画一个填充结构,或者清除一个区域的 content。

如果矩形不是你的风格,也许你会更感兴趣地画一个圆?画布实际上认为圆是弧的一个特殊情况。因此,要画一个半径为 50px 的圆,你需要指定的不仅仅是中心点和半径,还有起始角度和结束角度。这样一个圆的代码如下:

context.arc(75, 75, 50, 0, 2*Math.PI);context.fill();

在这里,中心点被指定为(75, 75),半径为 50。第四个术语是起始角度,我们给出为 0,而2*Math.PI是结束角度——整个圆周。一个可选的最终参数确定是否按逆时针方向绘制弧。它默认为false顺时针

警告最终参数会给出不同的弧,如下面的屏幕截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

正如你所看到的,画布中使用的所有角度都是以弧度表示的。要将角度(以度为单位)转换为弧度,你可以乘以Pi/180

对于更复杂的形状,画布支持直线或路径。要绘制一条线,你需要设置一个起始坐标,然后指定一个结束点,最后调用stroke()函数。画布会推断出在哪里绘制线条,如下面的代码片段所示:

context.beginPath();
context.moveTo(0,0);
context.lineTo(50,120);
context.closePath();
context.stroke();

在这里我们开始一个新的路径;笔移动到(0, 0),然后绘制到(50, 120)的直线,然后结束路径。开始和结束路径很重要,否则后续调用stroke()函数将从最后一个点继续绘制。你可以把它想象成使用一支笔;beginPath把笔放在纸上,moveTo暂时抬起笔移动,lineTo把笔移动到目的地并绘制线条,最后closePath把笔从纸上拿起来。不拿起笔,下次你绘制到某个地方的线条时,笔已经在纸上了,你会得到多余的一条线。

如果你认为绘制线条的语法有些复杂,你并不孤单。多次调用使你可以构建具有多个段的更复杂线条。通过使用循环,我们可以构建相对复杂的形状,如下面的代码片段所示:

var width = 200;
var height = 200;
context.moveTo(0, 0);
for(i = 0; i> 100; I += 4)
{
   context.lineTo(i, height-i);
   context.lineTo(width - 1-i, height - 1 -i);
   context.lineTo(width - 2-i, i+2);
   context.lineTo(i+3, i+3);
}
context.stroke();

这段代码将生成一个螺旋。循环的每次迭代,我们向图像中心移动 4 个像素,每个边缘一个像素。结果如下面的屏幕截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

画布在调用之间保持状态,所以,如果你使用context.fillStyle设置填充颜色,所有后续的填充形状都将采用相同的填充样式。正如你所想象的,这在使用画布构建可视化时是一个常见的 bug 来源。特别是在调用操作画布的函数时,这个问题尤为严重。

幸运的是,有一个简单的解决方案:上下文状态可以在进入和退出函数时保存和恢复。保持你的函数不干扰全局状态是一种礼貌,这肯定会减少 bug 的数量:

function fillCircle(context, x, y, radius){
   context.save();
   context.fillStyle = "orange";
   context.beginPath();
   context.arc(x, y, radius, 0, 2* Math.PI);
   context.fill();
   context.restore();
}

在函数的第一行,当前的绘图上下文被推入一个堆栈,然后在函数的最后一行恢复。通过这样做,我们可以在方法内部对上下文进行尽可能多的更改,并确信调用者的上下文不会被破坏。

Canvas 支持完整的颜色调色板,还允许透明度和甚至渐变。到目前为止,示例中使用的是颜色名称。这些名称可以追溯到 1990 年代中期,实际上源自 X11 窗口系统。然而,还有两种其他方法可以为 canvas 指定颜色。第一种是使用十六进制字符串,指定红色、绿色和蓝色的两个十六进制数字值。值越大,该颜色的强度越高,如下面的图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最后一种,也是我更喜欢的,是使用十进制 RGB 值,如下面的图所示,我认为这要清晰得多,而且编程时也更容易构建:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在十进制格式上有一个轻微的变体,即使用 rgba 函数而不是 rgb。这增加了一个额外的参数,这是一个介于 0 和 1 之间的十进制数,表示不透明度。1 表示完全不透明,0 表示完全透明。

由于 canvas 是一个基于栅格的绘图系统,因此可以在其中包含大多数其他栅格文件。栅格文件格式包括 JPEG、PNG、GIF 和 BMP。能够导入现有图像对于可视化来说是一个非常方便的工具。

然而,在 canvas 上使用图像有一些注意事项。当你包含一个图像时,你不能直接从 URL 加载它。首先,需要创建一个 JavaScript Image 对象,并将该图像的来源设置为 URL。这个 Image 对象然后可以用于画布上:

Var image = new Image();
image.src = "logo.png";

从其他域请求图像可能会有点棘手。为了保持浏览器在请求数据时的安全性,其他域受到限制。对于图像,可以通过设置图像的 crossOrigin 属性来请求托管域的使用权限:

Var image = new Image();
image.crossOrigin = "anonymous";
image.src = "http://codinghorror.typepad.com/.a/6a0120a85dcdae970b0128776ff992970c-pi";

在这里,crossOrigin 策略的值被设置为 anonymous。这意味着浏览器不会将任何身份验证信息传递给托管图像的服务器。如果您确实希望传递身份验证信息,也可以设置一个值为 user-credentials。对图像的支持相对较新,所以您最好将图像托管在与 canvas 相同的域上。

因为加载图像可能需要一些时间,所以不建议在 image 对象的 src 上设置值,并立即尝试在画布上绘制它。相反,你应该连接到图像的 onload 事件:

function drawImage()
{
   var img = new Image();
   img.src = 'worksonmymachine.png';
   img.onload = function(){
      var context = document.getElementById('example').getContext('2d');
      context.drawImage(img, 0, 0);
   }
}

使用 onload 函数可以防止将空图像渲染到 canvas 上。如果您正在加载多个图像,它们被加载的顺序是不确定的。您可能希望检查所有图像是否已加载再继续。复杂的依赖链可以使用 jQuery 的 Deferred 功能进行管理。drawImage 的第二和第三个参数,不出所料,是指定绘制图像坐标的。drawImage 还有更高级的版本,允许在将图像绘制到画布之前对图像进行缩放和裁剪。

我们最后要看的画布特性是转换。当组合更复杂的场景或可视化时,通常更容易以不同的比例、位置或方向构建对象。转换提供了一种改变画布上绘制形状的机制。

在这个上下文中的函数,即缩放函数,将把每个坐标乘以 x 或 y 的缩放因子。画布为独立缩放 x 和 y 值提供了支持。这意味着很容易只在一个方向上拉伸形状:

var colours = ["rgba(255,0,0,.5)", "rgba(0,255,0,.5)", "rgba(0,0,255,.5)"];
for(var i = 0; i< 3; i++)
{
    context.fillStyle = colours[i];
    context.scale(i+1,i+1);
    context.fillRect(10, 10, 50, 50);
}

前面的代码,仅仅绘制了一系列三个矩形,将产生如下屏幕截图所示的输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

你会注意到我们的缩放不仅仅是生成了更大的图像,它还放大了矩形的坐标。另外要注意的是,尺寸的增加似乎并不是均匀的。这是因为画布是状态 ful 的。如果你在系列中应用了几个缩放函数而没有重置缩放,每个都会建立在前一个缩放的基础上。你可以跟踪你的转换并应用一个逆转换函数。例如,如果你按 2 倍缩放,你可以按 2 的乘法逆,即 1/2,来回到原始缩放。更可能的是,保存上下文并使用我们之前提到的saverestore函数恢复它要容易些。

如果我们修改我们的函数,你可以看到结果图像非常不同,如下面的屏幕截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

经常在应用缩放转换时,你会想应用一个平移转换来将起始坐标移动到你期望的位置。你可以通过使用平移转换来实现:

var x = y =10;
var width = height = 100;
for(var i = 2; i><= 0; i--)
{
   var scalingFactor = i+1;
   context.save();
   context.fillStyle = colours[i];
   context.translate(x * -i, y *-i);
   context.scale(scalingFactor, scalingFactor);
   context.fillRect(x, y, width, height);
   context.restore();
}

在突出显示的行上,我们将画布向右移动,使其好像矩形在原点处绘制。转换应用的顺序很重要,如下面的屏幕截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在左侧,你可以看到我们代码的输出,在右侧,交换translatescale操作的结果。

画布还有很多其他很棒的特性。一一介绍实在太多,对于这么长的书来说;这很可能是一本自己的书。

可缩放矢量图形

可缩放矢量图形,或者如人们更常见的称呼,SVG,是 HTML 中相对较新的特性。它们执行的角色与画布类似,但不同之处在于它们是基于矢量而非位图的。这意味着每个图像都由一系列基本形状组成。这听起来可能有点像画布,毕竟,我们是用基本形状创建了所有的画布图像。不同的是,在 SVG 中,基本形状在绘制后仍然是独立的对象。而在画布上,创建图像的源命令在渲染后就会丢失。关于像素源的信息在画布命令的混乱中丢失。

方便的是,SVG 作为 XML 文件存储。虽然我通常连保险箱的组合锁都不会考虑存储,但这种难以访问的文件格式与 HTML 结合得很好。位图图像通常与 HTML 在单独的文件中链接。SVG 可以直接嵌入到 HTML 文档中。这种技术可以减少在用户代理上渲染页面所需的服务器请求次数。然而,真正的优势在于它允许 SVG 集成到 HTML 文档对象模型DOM)中,让你可以使用与操作任何其他元素相同的技巧来操作 SVG。

一个简单 SVG 的源代码可能如下所示:

<svg  version="1.1">
  <rect width="50" height="150" x="20" y="20" stroke="black"stroke-width="2" fill="#a3a3a3" />
</svg>

你可以将这复制到任何 HTML 文档中,你会得到一个如下截图所示的简单矩形:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码相当容易理解,其语法对于任何构建过网站的人来说都应该很熟悉。我们首先打开一个新的 SVG 元素。由于没有显式的尺寸信息,SVG 会填满其容器。在 SVG 内部,我们创建了一个宽度为 50px、高度为 150px 的新矩形。矩形的轮廓是黑色的,宽度为 2px,而内部则填充了灰色。

你可以用来构建图像的基本元素应该对你来说现在有些熟悉,既然你已经看到了画布的实际应用。rectpath从画布继承而来,没有发生变化。然而,SVG 在处理圆形方面有所不同,它提供了实际的<circle>标签,以及用于具有两个焦点的圆形形状的<elipse>标签。<polygon><polyline>标签用于绘制具有自由直边的形状,其中多边形是填充形状,而多段线只是一条线。如果你想要更弯曲的形状,SVG 提供了一个path元素,允许定义复杂的曲线和弧。手工构建曲线路径非常棘手。通常,对于曲线路径,你会想使用一个编辑器或 SVG 库。最后,SVG 支持使用恰当地命名为<text>的元素编写文本。

在 SVG 中构建多个元素就像向 SVG 中添加另一个子元素一样简单,如下面的代码片段所示:

>svg  version="1.1"<
  >rect width="50" height="150" x="20" y="20" stroke="black" stroke-width="2" fill="#a3a3a3" /<
  >rect width="50" height="120" x="75" y="50" stroke="black" stroke-width="2" fill="#a3a3a3"/<
  >rect width="50" height="90" x="130" y="80" stroke="black" stroke-width="2" fill="#a3a3a3"/<
  >rect width="50" height="60" x="185" y="110" stroke="black" stroke-width="2" fill="#a3a3a3"/<
>/svg<

这会导致图像看起来像下面这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

正如你所看到的,代码相当重复。在每一个矩形上,我们都指定了strokefill信息。这种重复可以通过两种方式消除。第一种是使用一组元素来定义样式信息。SVG 提供了一个通用的分组容器,由<g>标签表示。样式信息可以应用于该容器,而不是个别元素:

>g stroke="black" stroke-width="2" fill="#a3a3a3"<
  >rect width="50" height="150" x="20" y="20"  /<
  >rect width="50" height="120" x="75" y="50" /<
  >rect width="50" height="90" x="130" y="80" /<
  >rect width="50" height="60" x="185" y="110" /<
>/g<

一个替代方法是使用 CSS 为你完成样式设计:

>style<
  rect {
    fill: #f3f3f3;
    stroke: black; 
    stroke-width: 2;
  }
>/style<
>svg  version="1.1" id="graph1"<
  >rect width="50" height="150" x="20" y="20" /<
  >rect width="50" height="120" x="75" y="50" /<
  >rect width="50" height="90" x="130" y="80" /<
  >rect width="50" height="60" x="185" y="110" /<
>/svg<

在前面的代码中,样式信息直接附加到所有类型为rect的元素上。通常,你希望避免使用像这样的广泛选择器,因为它们将应用于页面上的所有 SVG 元素。最好通过使它们仅适用于那个一个 SVG,或者更佳,通过为希望样式的 SVG 元素分配一个类来缩小选择器。通常,建议使用 CSS 样式化你的元素,即使它们是 SVG 的一部分。你的 SVG 很可能包含多个你不想采用相同样式的矩形。

在 CSS 中使用的样式属性(fill, stroke等)与 SVG 标记中使用的并无不同。还有更先进的 CSS 选择器可用,如nth-child,它仅选择匹配特定模式的子元素。考虑以下代码片段:

rect:nth-child(even)
{
   fill:#878787;
}

在我们的示例中添加前面的代码将非常简单地创建我们图形的斑马条纹效果,如下面的屏幕截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们甚至可以更花哨地使用 CSS 通过在 CSS 中指定:hover伪选择器并改变光标下的颜色来为我们的图形添加一些交互:

rect:hover
{
   fill: rbg(87,152,176);
}

下面的屏幕截图展示了结果图形:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当然,将 SVG 作为 DOM 的一部分打开了除了样式以外的其他门。还可以使用 JavaScript 操作 SVG 的元素。你甚至可以为 SVG 元素分配事件监听器。

通过使用出色的 jQuery 库,我们可以轻松地为到目前为止我们构建的图 SVG 中的节点添加事件监听器:

$("rect").click(function(){
    alert($(this).attr("height"));
});

如果你以前从未见过 jQuery,那么在这里发生的事情是,我们选择了页面上的所有rect元素,当点击事件触发时,它会打开一个带有我们点击的列的高度的警告框。

在这本书中,我们将广泛使用 jQuery 库和这种基于 lambda 的编程风格。如果你不熟悉 jQuery,建议暂时停下来阅读一些教程,比如try.jquery.com/

我们已经涵盖了 SVG 的所有基本功能,但我想提一下的一个高级功能是:滤镜。滤镜是可以应用于 SVG 元素的转换。这些滤镜超越了我们在画布上看到的缩放和平移转换,尽管scaletranslate在 SVG 中都是支持的。大约有 20 种这样的滤镜,每一种都执行不同的转换。我们不可能一个个深入讲解,但我们会看其中几个。

可视化中最常见的需求之一就是给物体赋予立体感。完美的 3D 效果可能很难实现,但我们可以通过使用阴影来欺骗眼睛。这些阴影可以通过三种不同滤镜的组合来创建:偏移、高斯模糊和混合。

要使用滤镜,我们首先需要定义它。filter元素可以定义为一系列依次应用的滤镜。为了弄清楚给阴影需要哪些滤镜,我们可以从阴影的属性反向工作。首先要注意到的是,阴影是从 casting 它们的东西上偏离的。为此,我们可以使用一个偏移滤镜,使元素向一个方向或另一个方向移动。你要将元素移动到哪里取决于你的光源位置。为了我们的目的,假设光源在 SVG 的上方和左侧。这将使阴影向下和向右投射:

>defs<
    >filter id="shadowFilter" width="175%" height="175%"<
     >feOffset result="offsetImage" in="SourceAlpha" dx="5" dy="5"/<
    >/filter<
>/defs<

在滤镜属性行中,我们需要为被过滤的元素指定一个高度,这个高度要大于原高度。如果不这样做,我们的阴影会在超出源对象边界时被大量剪切掉。在这里,我们给滤镜指定了一个 ID,以便以后容易应用。你还会注意到,我们为feOffset指定了inout属性。这允许我们将滤镜串联起来。在我们的案例中,我们使用SourceAlpha,这只是原图的 alpha,或者说是原图的透明度属性。

让我们将这个滤镜应用到我们图表中的一个元素上,看看它会发生什么变化。我已经移除了其他样式,以免混淆。滤镜通过使用filter属性并给出先前创建的滤镜的 ID 来应用:

<rect width="50" height="60" x="185" y="110" filter="url(#shadowFilter)"/>

以下将是结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

阴影也比原图更模糊。这可以通过使用高斯模糊滤镜来实现:

<feGaussianBlur result="blurredOffset" in="offsetImage" stdDeviation="8" />

高斯滤镜会根据正态分布函数在图像内部随机移动点。你可能想尝试不同的模糊效果,我发现在 8-12 范围内的标准差对于阴影来说很不错:

<feBlend in="SourceGraphic" in2="blurOut" mode="normal" />

最后,我们想要创建的模糊黑色盒子与原图结合:

<feBlend in="SourceGraphic" in2="blurredOffset" mode="normal" />

在悬停时应用这个滤镜可以产生非常逼真的弹出效果,如图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

SVG 提供了通过你已经熟悉的 CSS 样式工具轻松操作图像部分的方法。同时,能够为图像附加事件允许创建令人印象深刻用户交互。

用哪个?

决定是使用画布还是 SVG 可能是一个难题。这主要取决于哪一个更让人感到舒适。有计算机图形或动画背景的人可能会更满意画布,因为画布的redraw循环会更熟悉。画布更适合重新绘制整个场景,甚至如果你计划使用 3D 元素。如果你的可视化利用了纹理或渲染的图像,画布能够直接将它们绘制到画布上,这几乎肯定会是有优势的。对于那些对保持快速帧率有依赖的可视化,画布通常表现良好。

另一方面,SVG 可以使用得更简单。SVG 中的每个元素都可以单独操作,这使得小动画变得容易得多。与 DOM 的集成允许在与 SVG 的单个元素交互时触发事件。在画布上实现这一点,你必须手动跟踪在该位置正在绘制的内容。SVG 也可以使用 CSS 进行样式设计,这使得组件更容易在具有不同主题的网站上重复使用。

为了本书的目的,我们将关注 SVG。SVG 的分辨率独立性,加上易用性和出色的支持库,使其成为一个合理的选择。我不相信有任何可视化是我们不能用画布创造的,但努力要大得多。尤其是对于交互式可视化来说,更是如此。

总结

你现在应该能够做出明智的决定,在 SVG 和画布之间构建简单的静态图像。在下一章,我们将暂时离开视觉方面,谈谈 OAuth 协议,该协议被许多社交媒体网站用来保护他们的数据。

第三章:OAuth

创建可视化只是战斗的一半;另一半是获取高质量的数据来驱动可视化。你可能希望利用许多潜在的数据来源。几乎每个国家都有一个负责收集和分析经济和社会统计的国家统计组织。在过去的几年里,许多政府开始采用开放数据计划。许多企业也以提供可用数据为中心;想想各个股票交易所提供的数据量。你可能甚至可以访问公司内部数据来推动你的可视化,或者你的可视化可能是更大应用程序的一部分,该应用程序将为你提供数据。

另一个来源,也是本书关注的来源,是社交媒体。社交媒体网站为其用户提供大量数据。绝大多数社交媒体网站提供 API 以编程方式访问数据。通常,这些数据是为你的用户账户定制的。例如,Twitter 根据你关注的用户过滤你看到的推文,Facebook similarly shows you updates from your friends. 有些数据是受限的,只有特定的人群可以看到它。你可能不希望全世界都看到你的 Facebook 更新,所以你设置了权限,只允许朋友看到它。为了定制数据,大多数社交网络网站要求对其 API 进行身份验证和授权。

通常,你想展示给用户的数据与他们自己的社交媒体账户有关。在大多数情况下,你的可视化不会托管在与你要使用的数据相同的系统上,如下面的图所示。更重要的是,你的可视化的消费者,即最终用户,可能还在另一个系统上:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这意味着需要有一种方法将从社交媒体网站获取社交媒体数据,然后发送给最终用户。从表面上看,似乎有以下两个选项:

  • 要求用户提供他们的社交媒体密码,并使用该密码进行身份验证,并向社交媒体 API 检查认证。

  • 要求用户获取用于可视化的数据,并将这些数据发送到你的可视化系统中。

这两个选项都不是特别理想。用户可能不愿意将密码告诉别人,尤其是像他们的社交网站这样重要的事情。你也不太想掌握他们的密码,因为这会给你带来额外的安全问题。大多数用户技术水平不足,无法从他们的社交网站提取所需数据并发送到你的应用程序。即使那些有技术技能的人也不会对这涉及的工作量印象深刻。当然,可以建立和优化标准导出机制,以使用户更容易操作,但在我们开发这个系统之前,也许还有第三种方法?

这个技巧似乎在于找到一种方法,从终端用户那里获取凭证信息,而不必向中介可视化网站透露太多细节。正如许多计算机问题一样,可能需要在计算世界之外找到一个平行的问题和解决方案。

许多电子车库门开启器和家庭安全系统提供访客代码。这些代码可能有一定的限制,比如只在工作日的某些时间或者一定次数内有效。这些账户的目的是为了提供有限访问你家中的权限,比如清洁工或工匠。一种类似的概念据称存在于高端汽车中:一种行李箱钥匙使汽车 operate in a restricted mode。可以使用类似的机制,向你可视化应用授予访问社交网站受限部分的权限。

OAuth 协议提供了一种机制,通过这种机制,应用程序可以在不知道你的密码的情况下,使用你的社交媒体数据和功能进行身份验证。

在本章中,我们将从高层次上了解 OAuth 是如何工作的,然后尝试使用我们现有的凭证对几个社交网络进行身份验证。

认证与授权

经常有人对认证和授权的概念感到困惑。认证是确保某人是他们所说的本人的行为,而授权是确保该人员有执行某动作的权利。这两个概念是相关的,并且经常是同一个步骤的一部分。OAuth 打破了两者之间的关系。尽管通常有一个认证步骤,需要登录到服务器,但认证的方式并没有规定。如果用户已经登录到服务器网站,授权步骤可能对用户是透明的。服务器可以使用任何方法进行认证。这为允许多因素认证甚至额外的认证委托打开了大门。例如,您的可视化可能需要从使用 OpenID 进行认证的 Stack Overflow 获取信息。因此,当用户请求访问 Stack Overflow 数据时,用户可能实际上需要使用他们的 Google 账户登录,该账户会将认证细节传递给 Stack Overflow,Stack Overflow 再将授权凭据传递给您的可视化。

在获取用于可视化的数据时,记住认证与授权之间的区别是很重要的。

OAuth 协议

在 OAuth 的引入和广泛更新之前,每个你需要与之交互的服务都提供了自己的授权协议。这些方法有很大的不同。每次你想利用一个新的 API 时,都必须学习并实现一个新的授权方案。这使得与大量服务的交互变得非常困难。

OAuth 的创建是为了解决与不同站点围绕授权缺乏标准化的问题。1.0 版本的创建花了大约两年时间,由多家主要行业参与者和一些较小感兴趣的方贡献而成。

OAuth 版本

目前 OAuth 在野生的两个主要版本:1.0a 和 2.0。版本 1.0a 是对 1.0 规范的安全更新,它修正了会话固定攻击。2.0 规范与 1.0a 有显著的不同,不幸的是,野生的服务中仍然混合使用了不同的协议。关于 2.0 规范的安全性存在一些政治辩论,这导致一些公司仍然停留在 1.0a 版本。最大的区别是 OAuth 1.0 旨在通过未加密的连接使用。当 OAuth 1.0 被创建时,SSL 证书对许多小提供商来说是非常昂贵的。因此,指定了一系列复杂的签名请求,即使是不安全的连接也可以表现得像安全的连接一样。

现在 SSL 证书已经变得便宜,硬件也足够快,以至于即使是最小的初创公司也负担得起 SSL 证书。因此,OAuth 2.0 版本依赖于 SSL 为其提供大部分安全性。在未加密的链接上使用 OAuth 2.0 是不安全的。对我们来说,我们可以 largely 忽略 OAuth 版本之间的其他差异。然而,你应该意识到,你为一家社交网站编写的授权和身份验证工具可能不会在其他的网站上工作。由于实现差异,你的授权方法甚至可能不会在两个完全支持 OAuth 2.0 的不同网站上工作。

OAuth 为我们在本章中看到的玩家定义了一些不同的名称,让我们使用这些术语。前一个图的更新视图如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

资源所有者是拥有数据的人。例如,你在 Facebook 上的更新“属于”你。客户端是请求访问数据的网站,服务器拥有资源所有者的数据和凭证信息。拥有的数据通常被称为受保护的资源,因为这是 OAuth 层所保护的。为了清晰起见,我保留了之前的位置中的图标,但客户端不必是可视化,服务器也不必是社交网站。

你可能已经使用了 OAuth 协议,即使你不知道它。如果你曾经授予一个应用程序使用你的 Twitter 账户的权限,你就使用了 OAuth。官方的 Twitter 应用程序,如TweetDeck,使用 OAuth 进行授权。每个应用程序请求从 Twitter 获取特定功能。如果你是 Twitter 用户,你可以在设置面板中查看你已经授权使用你的 Twitter 账户的应用程序。每一个这样的应用程序都被授权以你的身份与 Twitter 互动,如下面的截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在之前的截图中,你会注意到每个应用程序旁边都有一个按钮,允许撤销该应用程序的访问权限。这是 OAuth 的一大优点——应用程序永远不会知道你的密码,因此你可以移除它们代表你行动的能力,而无需更改你的密码。如果你确实希望更改你的 Twitter 密码,你只需要通过 Twitter 更改,而不需要更改所有你已经授权代表你行动的服务。

如果 Twitter 意识到,比如说www.wordpress.com已经被攻陷,他们可以一次性为所有用户撤销该应用程序的访问权限。保管凭证是一个困难的问题,并不是许多开发者愿意承担的问题。如果凭证可以被像 Twitter 这样的可靠公司保留,那么这就移除了一个常见的失败点。

让我们更深入地了解一下 OAuth 实际是如何工作的。为了理解正在发生的事情,走一遍示例是有用的。在这个示例中,我们的可视化将使用其 Graph API 从 Facebook 请求一些信息。Graph API 是 Facebook 提供给开发者的接口,用于访问社交图谱,这其实只是一个关于用户的属性集合。Facebook 是一个 OAuth 2.0 网站,所以这个示例将使用 OAuth 2.0 描述的工作流程。

我们的可视化希望访问 Facebook 的信息。我们第一次加载可视化时,会看到一个屏幕,用户可以通过点击按钮来获取 Facebook 的信息,如下面的屏幕截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当我们第一次设置可视化网站时,我们将从 Facebook 请求一个认证令牌。这个令牌是 Facebook 唯一授权给我们的网站的。在向 Facebook 注册应用程序的过程中,我们将输入一个域名,该域名可以用来使用令牌。这为防止其他人使用我们的令牌提供了一定的安全性。服务器可能执行广泛的检查,以验证应用程序是否有权访问其保护的数据。

我们的网站将向 Facebook OAuth 端点生成一个请求,该请求将包括生成的令牌。一个典型的 URL 可能如下所示:

https://www.facebook.com/dialog/oauth?client_id= 591498037422486&redirect_uri=http://visualization.com/&response_type=token

小贴士

关于如何通过 Facebook 认证的更详细示例可以在第七章Facebook中找到。

可视化现在直接重定向到 Facebook 登录页面,该页面会要求你输入登录信息,如下面的屏幕截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一旦你输入了相关信息并通过 Facebook 正确认证,你将被重定向到你 在redirect_uri参数中指定的页面。这通常会是你的原始页面。URL 后面将附加一个由 Facebook 生成的非常长的令牌,用于与他们的 API 进行后续通信:

http://localhost:8080/Visualization1.html#access_d8Ava9mMBALMG0FQp22SEn5La7mtC27evICrZB5ToVHdJRZC2FYdFmnIsveVKhcikSeVZAEAZAHBliKEeGvrKHnb5FnU5VoCooy49FIoJuzca3oHeYuNUZAatdgjUEr2tDzZBB8CJGmPkHdmNe3RyS1l9XAcTKwGGVAy6FB0gZDZD&expires_in=6667

根据你的可视化请求的权限,Facebook 可能会提示你明确地为可视化授予这些权限。向应用程序授予权限是 OAuth 的核心功能。Facebook 有大约 50 种不同的权限,你的应用程序可以请求。这包括以下内容:

  • email

  • user_likes

  • user_location

授予应用程序访问你 Facebook 信息的提示如下面的屏幕截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这个示例中,示例可视化请求访问我的位置,该位置受到user_location权限的保护。在 Facebook 的情况下,授予的权限被编码在要使用的令牌中,但这在 OAuth 协议中并没有明确说明。

一旦分配了权限,Facebook 会将用户重定向回你的redirect_uri,,让你利用令牌查询 Facebook API,如下面的屏幕截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于某些 OAuth 网站,在此阶段建议执行额外的调用,以确保返回的令牌与当前应用程序匹配。通过发送我们应用程序的 ID 和认证步骤返回的令牌,我们可以获得一些用于确认登录是否被劫持的验证详细信息。并非每个网站都需要这一步;这只是 Facebook 建议增加的一个安全措施。有了凭据,我们可以调用 Graph API。在此例中,可视化非常简单地向获取认证用户的姓和名的请求。您可以在第七章 Facebook 中看到请求是如何构成的。

就这样!所以 OAuth 将授权和认证步骤委托给第三方,无需复杂工具。OAuth 只使用正常的 HTTP 和 SSL。OAuth 的工作流程在以下图中几乎完全表示出来:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

OAuth 的各种方言和要求可能难以理解。找到与不同网站正确认证的令牌的正确组合几乎和 OAuth 之前的混乱技术一样困难。OAuth 以其难以理解和不一致而闻名。这可以归因于 OAuth 标准并非像 HTTP 那样的标准。通过 HTTP,如果你遵守标准,你可以确信你的服务能够与其他所有服务进行交互。OAuth 不提供那种级别的互动保证。

如果您要使用很多不同的数据源,也许您正在创建一个 mashup,那么建议您使用第三方服务与 OAuth 服务器通信。即使您只使用一个数据源,您可能也不希望将 OAuth 的复杂性引入您的开发过程。DailyCred 和 OAuth.io 等公司提供了一个服务,消除了处理 OAuth 的困难。他们通过一致的 API 允许与多个不同的 OAuth 提供者进行认证。适应各种 OAuth API 的艰苦工作由他们完成,让您能够专注于构建可视化。

使用 OAuth.io 对 Facebook 进行认证只需运行以下几行代码那么简单。

OAuth.initialize('<Your Public key>');
OAuth.redirect('facebook', "callback/url");

本章中使用的示例可视化代码接近 70 行。

当然,这些服务具有一定的货币成本,并且还提供了一个额外的失败点。和所有事情一样,要确保选择一个适合 OAuth 的好解决方案。

摘要

虽然并非每个社交媒体网站都使用 OAuth,但了解 OAuth 的工作原理以及如何用它来简化 API 使用,很可能会提升你在开发可视化过程中的体验。你现在应该能够解释 OAuth 是如何工作的。在下一章,我们将探讨可视化的库。

第四章.用于可视化的 JavaScript

在第二章《JavaScript 和 HTML5 用于可视化》中,我们探讨了使用可缩放矢量图形构建我们可视化所带来的优势。然而,构建 SVG 通过操作底层的 XML 是一个令人沮丧且耗时的练习,尽管有许多 XML 操作工具,但利用专门为构建 SVG 设计的 API 而不是更一般的语言会更好。

有许多 JavaScript 库是为了操作 SVG 而创建的。svg.js(www.svgjs.com/)和 Raphaël(raphaeljs.com/),都值得提及,因为它们是绘制 svg 图形的优秀工具。Raphaël 网站上的演示特别令人印象深刻。d3.js 提供了专门为可视化设计的功能,我们也会看看那个。

Raphaël

利用Raphaël绘制简单的矩形比用 XML 构建相同的矩形要方便得多。这个库可以从硬盘或者像 CloudFlare 这样的 CDN 中包含,如下面的代码所示:

<html>
  <body></body>
  <script src="img/raphael-min.js"></script>
</html>

我们可以用以下方法绘制任何形状:

function drawRectangle()
{
  var paper = Raphael("visualization", 320, 200);
  paper.rect(50, 20, 50, 150);
}

这段代码找到 ID 为visualization的元素,并向其添加一个 320x200 像素的 SVG。然后,它在(50, 20)处插入一个新的矩形,宽度为50,高度为150

如果我们想用 Raphaël 创建一个简单的柱状图,那将不会很难。让我们试一试。我们现在需要的第一件事是些数据。我们可以先用 JavaScript 数组,但在现实世界中,这些信息可以从 Web 服务中以 JSON 或 XML 的形式获取。在这个例子中,我们将选择一些月份及其相关值,如下面的代码所示:

var data = [{month: "May", value: 5},
  {month: "June", value: 4},{month: "September", value: 8}];

现在,让我们更新上面使用的用于绘制矩形的函数来处理数据值。首先,我们将更改方法签名,如下面的代码所示:

function drawGraphColumn(paper, item, currentColumn, maximumValue)
{}

这个函数接收要绘制的 SVG、当前项目和一个用于计算适当高度的最大值。在函数体中,我们将首先计算条形图的高度,如下面的代码所示:

var barHeight = (500 * (item.value/maximumValue));

我们硬编码了最大高度为 500 像素,每个条形图仅仅是该高度的百分比,等于项目的值占最大值的百分比。我们将使用这个值来绘制条形图,如下面的代码所示:

var rectangle = paper.rect(currentColumn*30, 500 - barHeight, 20, barHeight);
rectangle.attr("fill", "rgb("+ (item.value * 40) + " ," + (item.value * 20) + "," + item.value * 20 + ")");
rectangle.attr("stroke-width", 2);

矩形根据它所在的列来偏移,以避免重叠的矩形。我们将颜色设置为项目值的函数,以便颜色随高度变化。

这个函数通过将我们每一个数据元素依次传递给它来调用:

var maximumValue = 0;
$.each(data, function(index, item)
{
  if(item.value > maximumValue) maximumValue = item.value;
});
$.each(data, function(index, item){
  drawGraphColumn(paper, item, index, maximumValue);
});

在这里,我们首先从数组中计算最大值。然后,为数组中的每个元素调用我们上面定义的drawGraphColumn()函数。通过 jQuery 的each操作符遍历数据数组,该操作符将给定的函数应用于数组中的每个元素。生成的图表如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Raphaël 是一个通用 SVG 库。这意味着除了适合构建可视化,它还可以用来创建更通用的绘图。正如我们寻找一个比手动操作 XML 更合适的 SVG 操作 API 一样,拥有一个针对构建可视化而设计的库会很好。d3.js 是一个专门设计用来简化 SVG 构建可视化的库。

d3.js

d3.js带来了一系列函数和编程风格,使得即使是上面的那种简单图表的创建也变得更简单。让我们用 d3 重新创建上面的图表,看看它们的区别。首先需要做的是在页面中引入一个 SVG 元素。在 Raphaël 中,我们使用构造函数来完成这个操作;在 d3 中,我们将显式地添加一个 SVG 元素,如下面的代码所示:

var graph = d3.select("#visualization")
  .append("svg")
  .attr("width", 500)
  .attr("height", 500);

立即,你会发现使用的 JavaScript 风格与 Raphaël 有很大的不同。d3 非常依赖方法链的使用。如果你对这个概念还比较陌生,很快就能掌握。每个方法调用执行一些操作,然后返回一个对象,下一个方法调用在这个对象上执行。所以,在这个例子中,select方法返回了 ID 为visualizationdiv。在选定的div上调用append方法添加一个 SVG 元素,然后返回它。最后,attr方法在对象内部设置一个属性,然后返回该对象。

起初,方法链可能看起来很奇怪,但随着我们的进展,你会发现它是一个非常强大的技术,能显著清理代码。如果没有方法链,我们将产生很多临时变量。

接下来,我们需要在数据数组中找到最大的元素。在之前的示例中,我们使用了 jQuery 的each循环来找到这个元素。d3 拥有内置的数组函数,使得这个过程变得更为简洁,如下面的代码所示:

var maximumValue = d3.max(data, function(item){ return item.value;});

寻找最小值和平均值也有类似的功能。这些函数没有任何是你在使用 JavaScript 工具库,如 underscore.js 或 lodash 时得不到的。然而,使用内置版本是很方便的。

我们接下来要使用的是 d3 的缩放函数,如下面的代码所示:

var yScale = d3.scale.linear()
  .domain([maximumValue, 0])
  .range([0, 300]);

缩放函数用于将一个数据集映射到另一个数据集。在我们的案例中,我们将从我们数据数组中的值映射到我们的 SVG 中的坐标。我们在这里使用了两种不同的比例尺:线性和序数。线性比例尺用于将连续域映射到连续范围。映射将线性完成,所以如果我们的域包含010之间的值,而我们的范围值在0100之间,那么值6将映射到60,值3将映射到30,依此类推。这看起来很简单,但是在更复杂的域和范围中,比例尺非常有帮助。除了线性比例尺之外,还有幂和对数比例尺,这些可能更适合您的数据。

在我们示例数据中,我们的y值不是连续的,甚至不是数字。在这种情况下,我们可以使用序数比例尺,如下面的代码所示:

var xScale = d3.scale.ordinal()
  .domain(data.map(function(item){return item.month;}))
  .rangeBands([0,500], .1);

序数比例尺将离散域映射到连续范围。在这里,domain是月份列表,范围是我们 SVG 的宽度。你会注意到我们没有使用range,而是使用了rangeBands。范围带将范围分成块,每个块分配一个范围项目。所以,如果我们的域是{五月,六月},范围是0100,那么五月起我们将收到从049的带,六月从50100的带将是june。你还会注意到rangeBands有一个额外的参数;在我们的例子中是0.1。这是一个填充值,生成每块之间的某种无人区。这对于创建柱状图或柱状图来说很理想,因为可能不希望柱子接触。填充参数可以取01之间的值,作为十进制表示保留多少范围用于填充。值为0.25将保留 25%的范围用于填充。

还有一些内置的比例尺用于提供颜色。为您的可视化选择颜色可能具有挑战性,因为颜色必须相隔足够远以被人辨认。如果你像我一样对颜色有挑战,那么比例尺category10category20category20bcategory20c可能适合你。您可以声明一个颜色比例尺,如下面的代码所示:

var colorScale = d3.scale.category10()
  .domain(data.map(function(item){return item.month;}));

之前的代码将为 10 种预计算的可能颜色中的每一个月分配一个不同的颜色。

最后,我们实际上需要绘制我们的图表,如下面的代码所示:

var graphData = graph.selectAll(".bar")
  .data(data);

我们使用selectAll选择图表内的所有.bar元素。等等!图表内没有与.bar选择器匹配的元素。通常,selectAll将返回与选择器匹配的元素集合,就像 jQuery 中的$函数一样。在这种情况下,我们使用selectAll作为创建一个空的 d3 集合的简写方法,该集合有一个data方法并且可以被链式调用。

接下来,我们需要指定一组数据与现有元素的数据集合进行联合。d3 操作集合对象,不使用循环技术。这使得编程风格更加声明式,但可能一下子不容易掌握。实际上,我们正在创建两个数据集的并集,现有数据(通过selectAll找到)和新数据(由data函数提供)。这种处理数据的方法使得数据元素的更新变得容易,如果后来又增加了或移除了元素。

当新的数据元素被添加时,你可以通过使用enter()只选择那些元素。这可以防止对现有数据执行重复操作。你不需要重新绘制整个图像,只需要绘制用新数据更新过的部分。同样,如果新数据集中有在旧数据集中没有出现的元素,它们可以通过exit()被选择。通常,你只需要移除那些元素,如下面的代码所示:

graphData.exit().remove()

当我们使用新生成的数据集创建元素时,数据元素实际上会被附加到新创建的 DOM 元素上。创建元素的过程涉及到调用append,如下面的代码所示:

graphData.enter()
  .append("rect")
  .attr("x", function(item){ return xScale(item.month);})
  .attr("y", function(item){ return yScale(item.value);})
  .attr("height", function(item){ return 300 - yScale(item.value);})
  .attr("width", xScale.rangeBand())
  .attr("fill", function(item, index){return colorScale(item.month);});

下面的图表展示了data()如何与新旧数据集一起工作:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

你可以在之前的代码中看到方法链变得多么有用。它使得代码变得更短,比分配一系列临时变量或把结果传给独立方法更易读。比例在这里也显示出了其独特之处。x 坐标简单地通过使用序数比例缩放我们拥有的月份来找到。因为这种比例考虑了元素的个数以及填充,所以不需要更复杂的东西。

它们的坐标同样是通过之前定义的yScale找到的。因为 SVG 的原点在左上角,我们必须取比例的逆来计算高度。再次强调,这个地方我们通常不会使用常数,除非是为了我们例子的简洁。柱状图的宽度是通过询问xScale带宽得到的。最后,我们根据颜色比例设置颜色,使其看起来像下面的图表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这个 d3 版本的图表实际上比 Raphaël 版本的功能要强大得多。我们通过使用比例消除了 Raphaël 中存在的大量魔法数字。

让我们继续增强我们的图表,并探索 d3.js 的其他一些功能。

自定义颜色比例

我们上面生成的 d3 图表颜色非常丰富,因为我们利用了内置的颜色比例之一。然而,大多数时候你的视觉化图表必须有一定的主题连贯性。你可以通过利用自定义比例来实现这种连贯性。

让我们从一个简单的例子开始:交替颜色刻度。为了创建我们新的颜色刻度,作为现有category10刻度的替换,我们需要使用一点 JavaScript 乐趣来向 d3 中注入一个新的刻度函数。我们首先将函数附加到 d3 的刻度命名空间,如下面的代码所示:

d3.scale.alternatingColorScale = function()
{

JavaScript 允许对对象进行猴子补丁(monkey patching),所以这个函数实际上会显示为是 d3 的一部分。我们将通过设置域和范围开始实现函数。我们定义了domainrange函数,它们作为域和范围的获取器和设置器:

var domain, range;
  scale.domain = function(x){
    if(!arguments.length) return domain;
    domain = x;
    return scale;
  }
  scale.range = function(x){
    if(!arguments.length) return range;
    range = x;
    return scale;
  }

最后,我们将设置一个映射函数,该函数在使用刻度时被调用,如下面的代码所示:

  function scale(x){
    return range[domain.indexOf(x)%range.length];
  }
  return scale;
}

这个刻度是通过以下代码应用的:

var colorScale = d3.scale.alternatingColor()
  .domain(data.map(function(item){return item.month;}))
  .range(["#423A38", "#47B8C8", "#BDB9B1"]);

这导致了一个看起来更一致的图表,如下面的图表所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我选择了最简单的条件来定制我们的刻度,但可以使用更复杂和信息量更大的刻度。一个使用阈值的刻度在以下图表中显示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这可以通过更改scale函数,并将值(而不是键(月份))传递给函数来轻松完成。

标签和坐标轴

到目前为止,我们构建图表时并没有太关注我们正在绘制的数据。如果在图表上加上一些标签,人们就可以轻松地解读我们的数据,那就太好了。幸运的是,d3 提供了一个axis()函数,使得添加坐标轴变得非常简单。

我们将从 x 轴开始,如下面的代码所示:

var xAxis = d3.svg.axis().scale(xScale).orient("bottom");
  graph.append("g")
  .attr("transform", "translate(20,300)")
  .attr("text-anchor", "middle")
  .call(xAxis);

我们首先使用axis函数创建一个坐标轴。我们将xScale传递给它,给它一个提示,关于刻度上的刻度应该放置在哪里。接下来,我们在图表中添加一个g元素。g是一个 SVG 元素,它作为一个容器来持有其他元素。你可以在g元素内放置任何其他形状,然后作为一个整体对它们进行变换。我们接下来就是这样做。默认情况下,坐标轴位于图表的顶部,所以我们把它向下和稍微向右移动,以更好地对齐。text-anchor属性设置了文本的 x 坐标应该固定的位置。默认情况下它是左边的,但由于我们每个条形的中间位置,我们设置text-align:middle。最后,我们传递了xAxis

yAxis通过以下代码添加起来也同样简单:

var yAxis = d3.svg.axis().scale(yScale).orient("left");
  graph.append("g")
  .attr("transform", "translate(20,6)")
  .call(yAxis);

这里的翻译要复杂一些,我们只是说明了我们给刻度提供了左方向,这使得它被绘制在最小刻度值的左侧。因为我们的最小刻度值是0,所以它被绘制在屏幕外,如下面的图表所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们设法在几行代码内添加了坐标轴和标签。使用更通用的 SVG 库,这将需要大量的工作。坐标轴函数也有许多配置选项。您可以设置刻度的数量、标签以及刻度的格式。

摘要

虽然我们已经接触了不少 d3 的内容,但我们对 d3.js 能做的很多事情还只是略知一二。它是一个庞大而强大的库。关于 d3 的主题有很多书,所以我们不可能全部覆盖。关于 d3 的一个很好的资源是 Swizec Teller 的书籍《使用 d3.js 进行数据可视化,Packt 出版社》。在我们开发书中剩余部分的一些可视化时,我们会发现许多额外的功能。我们还将访问本章中介绍的一些函数的进一步应用。

第五章:Twitter

Twitter 是一个真正在开放和可用的 API 上成长起来的服务。最初,没有 Twitter 客户端。与 Twitter 的通信限于 SMS,后来是网站。在 Twitter 的开发过程中,开发者显然累积了数百美元的 SMS 费用,用于测试和构建系统。Twitter 在数百名开发者的支持下变得流行,这些开发者使用开放的 Twitter API 或 Twitter RSS 源构建了工具,如TweetDeckTweetree。因此,Twitter 提供了一个丰富的 API,可以用来构建应用程序,在我们这个案例中,是可视化。

在我们开始构建可视化之前,让我们来看看 Twitter API,以及我们如何使用它。在dev.twitter.com上有大量的 API 文档可供查阅。如果你需要更多信息,这应该是你进行额外研究的第一个停靠点。

对于我们的目的,Twitter 提供了两种不同的数据获取模型。第一种是典型的 RESTful 模型,其中客户端向 Twitter 请求特定资源,这些资源通过 HTTP 以 JSON 格式返回。这个 API 可能与你之前见过的其他 API 相似。它是无状态的,意味着在请求之间不会在服务器端保留任何信息,并遵循 HTTP 的最佳实践。如果你试图在网页浏览器中消费 Twitter 数据,那么这个选项适合你。第二种选择是流式 API。这种方法利用一个持久的 HTTP 连接,Twitter 会在消息发生时向该连接发送消息。通常不建议从浏览器使用流式 API,所以你需要一个浏览器和 API 之间的中介服务器,如下面的图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

不幸的是,拥有服务器是使用 Twitter 的所有可视化的要求,即使是 RESTful API,因为 Twitter 不支持使用纯基于浏览器的解决方案进行身份验证。我们稍后会深入讨论所有这些,但首先我们需要在 Twitter 上设置一个开发者账号。

获取 API 访问权限

如果你记得在第三章中,OAuth,其中一个要求是为我们想要与之通信的每个网站获取一个应用程序密钥。这适用于 Twitter,所以我们去这样做。

打开浏览器,前往dev.twitter.com,然后点击登录链接。如果你已经有一个 Twitter 账号,那么你可以在这里使用它来登录。如果没有,你可以注册一个新的 Twitter 账号。别担心,这些都是免费的。

登录后,然后在右上角应该有一个链接到我的应用,如下面的屏幕截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击那个链接会带你到一个页面,你可以在那里设置你的第一个应用程序。你需要为应用程序输入一些信息。对于大多数字段,你可以输入你选择的任何内容,但回调 URL 应该是127.0.0.1:8080/twitter1.html。这是 Twitter 完成 OAuth 阶段后将要引导你访问的 URL。在这里我们使用 localhost 值,但在生产环境中,你希望使用你可视化的公共面向 URL。下面的屏幕快照显示了应用程序详情窗口:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

提示

在这里你不能使用 localhost 域名,但如果你不想看到 IP 地址,那么你可以使用一个 URL 缩短服务为你的 localhost URL 创建一个别名。确保你的 URL 缩短器保留查询参数,否则你将无法正确登录。

一旦你的应用程序创建完成,你将能够从我们之前使用的同一个我的应用程序选项中看到各种设置。对我们目的来说,关键信息是OAuth 设置,如下面的图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这些密钥将用于用户对我们应用程序的授权。如果密钥泄露了—例如,你可能把它粘贴到你正在写的书里—你可以通过重置密钥链接来重新设置。这样做可以防止你的书的读者假装是你,并在你不知情的情况下为你做出不可描述的坏事。

设置服务器

如我所说,Twitter 不允许从浏览器直接访问其认证结构,我们需要使用一个服务器。幸运的是,我们可以在自己电脑上的服务器上进行开发——不需要外部服务器。这本书中使用了大量的 JavaScript,所以让我们继续这个主题,使用 node.js 在本地托管我们的网站。任何其他的 HTTP 服务器也可以工作。

安装 node.js 相当简单。如果你使用的是 Windows,那么可以从nodejs.org下载安装程序。在 OS X 上,可以在同一个网站上下载基于.pkg的安装器,或者使用 Homebrew 进行安装。如果你使用的是 Linux,最好是从源代码编译。然而,如果你使用的是带有内置包管理系统的发行版,例如aptyum,那么可以使用这两个命令中的任何一个来安装 node.js 包:

sudo yum install nodejs            #Fedora or RedHat
sudo apt-get install nodejs        #Debian or Ubunt

node.js 是一个设计为异步执行所有 I/O 任务的服务器端软件。这意味着像写入磁盘这样的操作是在不阻塞主线程的情况下处理的。当 I/O 完成后,主线程会被通知。最常用的应用之一是将其用作 HTTP 服务器。这个功能以 HTTP 模块的形式包含在盒子里,但该模块提供的接口相当轻量级。相反,我们将使用 Express 框架。Express 是一个轻量级框架,提供了一些围绕路由、会话和内容服务的基础设施,以及模板。它可以通过 node 包管理器npm安装,如下所示命令:

npm install express

接下来,我们将使用 Express。

OAuth

OAuth 当然可以手动配置和控制,但我们站在巨人的肩膀上有充分的理由。对我们来说,使用一个已经建立的 OAuth 库要容易得多。幸运的是,node 有一个这样的库,创造性地称为OAuth。即使有了这个库,你也会看到与 OAuth 1.0a 端点的交互是复杂的。要安装它,再次切换到命令行并使用 node 包管理器:

npm install oauth

这个库可以执行 OAuth 1.0a 和 OAuth 2.0 操作。由于 Twitter 是一个 OAuth 1.0a 端点,我们将使用它。

首先,我们需要设置我们的 Express 应用程序。Express 提供了应用程序模板,但对于本章中的简单应用程序来说,这些模板过于复杂。如果你计划在将来创建一个更复杂的应用程序,那么你可能需要更深入地了解应用程序生成和目录结构。我们从以下代码中要求express并使用加载的模块创建一个新应用程序:

var express = require("express");
var app = express();
var oAuth = require('oauth');

Require 是一个库,它允许动态加载 JavaScript 库。这是在 node 应用程序中引入外部模块的最常见方式。接下来,我们在express中配置了一些设置,如下所示代码:

app.configure(function() {
  app.use(express.bodyParser());
  app.use(express.cookieParser() );
  app.use(express.session({ secret: "a secret key"}));
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});

bodyParser 允许 express 对发送到服务器的请求体进行简单的解析。下一行,设置了 cookieParser。与 bodyParser 类似,这允许解析 cookies 并将从 cookies 中检索到的值填充到请求对象中,在我们的案例中,是会话信息。接下来,我们设置了会话功能。这允许我们从一个请求到另一个请求共享信息。在其默认配置中,它使用内存存储来持有会话信息。这意味着重新启动您的应用程序将清除会话信息。如果您在一批机器上托管您的可视化,您将需要使用外部数据存储机制,如MongoDBRedis。我们传递了一个密钥,该密钥用于生成 session 哈希。它应该是一个随机字符串。使用 app.router 将指示 express 监听路由请求,我们将在一秒钟内定义这些请求。最后,我们的 .html.js 文件将位于一个名为 public 的目录中,因此我们将指示 express 作为静态资源提供该目录的内容。

现在我们想使用 OAuth 库。这可以通过以下代码中的函数完成:

function getOAuth(){
  var twitterOauth = new oAuth.OAuth(
  'https://api.twitter.com/oauth/request_token',
  'https://api.twitter.com/oauth/access_token',
  consumerKey,
  consumerSecretKey,
  '1.0A',
  null,
  'HMAC-SHA1');
  return twitterOAuth;
}

我们创建了一个与 Twitter 关联的 OAuth 对象。我们给出了两个端点,然后是之前从 Twitter 收到的消费者密钥和消费者密钥。OAuth 1.0a 需要嵌入消费者密钥这就是为什么不能使用客户端代码从 Twitter 检索信息的原因。消费者密钥不能泄露给外人,因为它会被发送到客户端。1.0A 作为 OAuth 的版本;不需要授权回调,所以第六个参数为 null。最后一个参数是签名方法:Twitter 使用 HMAC-SHA1

接下来,我们在 Express 应用程序中设置一个请求 Twitter OAuth 令牌的路由:

app.get('/requestOAuth', function(req, res){
  function recieveOAuthRequestTokens(error, oauth_token, oauth_token_secret,results) {
    if (!error){
      req.session.oAuthVars = { oauth_token: oauth_token,oauth_token_secret: oauth_token_secret}; res.redirect('https://api.twitter.com/oauth/authorize?oauth_token=' + oauth_token);
    }
  requestOAuthRequestTokens(recieveOAuthRequestTokens);
});
function requestOAuthRequestTokens(onComplete){
  getOAuth().getOAuthRequestToken(onComplete);
}

在这里,我们将 /requestOAuth 路由首先请求一个 OAuth 令牌,然后使用该令牌将用户重定向到 Twitter 的登录页面。我们构建了一个匿名函数并将其传递给 OAuth,因为 node 是非常异步的。回调模型允许主线程在等待 Twitter 回复 OAuth 令牌的同时服务于另一个请求。一旦我们有了 OAuth 令牌,我们将其保存在会话状态中以供下一步使用,并将用户重定向到 Twitter 授权页面。

Twitter 将在用户完成认证后重定向到我们设置应用程序时定义的 URL。在我们的案例中,这将由路由 /receiveOAuth 服务,如下面的代码所示:

app.get('/receiveOAuth', function(req, res){
  if(!req.session.oAuthVars){
    res.redirect("/requestOAuth");
    return;
  }
  if(!req.session.oAuthVars.oauth_access_token){
    var oa = getOAuth();
    oa.getOAuthAccessToken( req.session.oAuthVars.oauth_token, req.session.oAuthVars.oauth_token_secret, req.param('oauth_verifier'),
    function(error, oauth_access_token, oauth_access_token_secret,tweetRes) {
      req.session.oAuthVars.oauth_access_token = oauth_access_token;
      req.session.oAuthVars.oauth_access_token_secret = oauth_access_token_secret;
      GetRetweets(res, req.session.oAuthVars.oauth_access_token, req.session.oAuthVars.oauth_access_token_secret);
    });
  }
  else
    GetRetweets(res, req.session.oAuthVars.oauth_access_token, req.session.oAuthVars.oauth_access_token_secret);
});

这段代码获取了 Twitter 的 redirect 返回的 OAuth 令牌,并执行了最后一步,即查找访问令牌。一旦我们有了这些访问令牌,它们就可以用来调用 API——在这里在 GetRetweets 函数中完成。我们将会在会话中保存生成的所有令牌,这样用户就不用不断地授权给 Twitter API。

是否对令牌感到厌倦了呢?你应该感到厌倦!设置 OAuth 1.0a 的这次交换使用了很多令牌。幸运的是,我们已经完成了令牌和 OAuth 的设置。现在我们可以开始用 Twitter 数据构建可视化了!

可视化

Twitter 向我们提供了一系列 API。我们应该或许先发明一些我们想要可视化的东西,然后决定数据是否可用以及我们如何展示它。我对我所关注的哪些人发推文最多感到好奇。一些账户,比如@kellabyte,似乎总是在发推文,而像@ericevans 的其他账户几乎不发推文。

服务器端

让我们先从服务器端获取数据开始。在 node.js 中,我使用以下代码设置了一个新路由:

app.get('/friends', function(req, res){
  if(!req.session.oAuthVars || !req.session.oAuthVars.oauth_access_token){
    res.redirect('/requestOAuth');
    return;
  }
  var cursor = -1;
  receiveUserListPage(res, req.session.twitterVars.user_id, req.session.oAuthVars.oauth_access_token, req.session.oAuthVars.oauth_access_token_secret, cursor, new Array());
});

首先,我们检查确保在会话中拥有合适的令牌。如果没有,则重定向回requestOAuth页面,这将启动整个 OAuth 工作流程。接下来,我们设置一个初始的游标值。Twitter 限制从其服务中返回的结果数量。这避免了向消费者推送一百万条记录,这双方都不太可能想要。对于 API 调用,我们将使用设置为 20 的限制。然而,Twitter 还提供了一个他们称之为游标继续令牌。通过使用这个令牌再次调用服务,将返回下一页的结果。初始值为-1,表示第一页。游标以及所有必需的令牌传递给receiveUserListPage,该方法将执行实际的查找操作。

提示

速率限制

Twitter 限制了您可以发送到他们服务的请求数量。在开发可视化时,您可能会遇到这些限制。等待 15 分钟,然后再次尝试。在生产环境中,尝试缓存您的数据,这样您就不必频繁地查询 Twitter。

receiveUserListPage看起来像以下代码:

function receiveUserListPage(res, user_id, oauth_access_token, oauth_access_token_secret, currentCursor, fullResults){
  var oauth = getOAuth();
  oauth.get( 'https://api.twitter.com/1.1/friends/list.json?skip_status=true&user_id=' + user_id + "&cursor=" + currentCursor,
  oauth_access_token, 
  oauth_access_token_secret,
  function (e, data, oaRes){
  var jsonData = JSON.parse(data);
  if(jsonData.errors){
    projectResults(res, fullResults);
    return;
  }
  fullResults = _.union(fullResults, 
  _.map(jsonData.users, 
  function(item){return { name: item.name, 
    count: item.statuses_count
  }}));
  if(jsonData.next_cursor == 0){
    projectResults(res, fullResults);
  }
  else
    ReceiveUserListPage(res, user_id, oauth_access_token, oauth_access_token_secret, jsonData.next_cursor, fullResults);
  }
});
}

function projectResults(res, fullResults)
{
  var selectedResults = _.first(_.sortBy(fullResults, function(item){return item.count;}).reverse(), 10);
  res.end(JSON.stringify(selectedResults));
}

我们首先获取 OAuth 库的引用,然后使用当前的游标和当前的user_id来查询 Twitter。我们使用的 API 调用返回了我关注的名单上的一组 20 个用户。结果以字符串形式返回,因此我们使用JSON.parse将它们解析为对象。如果结果对象包含一个名为errors的字段,那么我们很可能遇到了速率限制,因此我们返回到目前为止我们所获取的所有内容。因为对于这个 API 调用,速率限制只有15,如果你关注超过 300 人,你将遇到限制。

如果我们有结果,我们将把它们添加到我们当前的数据集中。我们使用 Underscore 的map函数只选择两个字段。这节省了带宽,使调试更容易,因为从 Twitter 返回的对象带有几十个无用的字段,非常沉重。如果next_cursor等于0,那么这意味着我们已经到达列表的末尾,可以返回当前的名字和计数集合。否则我们重新进入函数,给它新的游标,名字集合和项目。一旦我们遇到可以返回的情况,我们调用projectResults,将拥有最多推文的 10 个用户以 JSON 格式发送给客户端。

顶端

Underscore.js

Underscore JavaScript 库是一个小型库,它使处理数组变得更容易。它添加了集合函数如unionintersect,以及映射和减少等功能编程概念。它可以从underscorejs.org/下载。

客户端

客户端可视化代码可以放在我们之前指导 Express 作为静态内容服务的公共目录中。

我想要直观地展示最活跃的推文者。一种很好的方法是使用气泡图,气泡越大,他们的推文就越多。让我们逐步构建代码:

function visualize(data){
  var graph = d3.select(".visualization")
  .append("svg")
  .attr("width", 1024)
  .attr("height", 768);
  var colorScale = d3.scale.category10();
  calculateBubbles(data, 1024, 768);
  var currentX = 0;
  graph.selectAll(".bubble")
  data(data)
  enter()
  append("circle")
  .style("fill", function(x,y){return colorScale(y);})
  .attr("cx",  function(d){return d.cx;})
  .attr("cy", function(d){ return d.cy;})
  .attr("r", 0)
  .attr("opacity", .5)
  .transition()
  .duration(750)
  .attr("r", function(d){return d.radius;});
  graph.selectAll(".label")
  .data(data)
  .enter()
  .append("text")
  .text(function(d){return d.name + "(" + d.count + ")";})
  .attr("x", function(d){return d.cx;})
  .attr("y", function(d){return d.cy;})
  .attr("text-anchor", "middle");
}

如今当你对 d3 有所了解后,这些代码看起来会很熟悉。传入的数据数组是我们从节点服务中检索到的。首先,我们在页面上创建一个任意的 SVG 元素。然后,我们设置一个颜色比例尺,以便我们的可视化效果颜色悦目。calculateBubbles函数是一个辅助函数,将计算气泡的位置。它用圆的 x 和 y 坐标以及半径增强我们的数据数组。我们在这里不会深入讲解那个,但代码可以在 GitHub 上找到。我们对每个顶级推文者创建一个气泡。我们使用颜色比例尺为其着色,并使用数据数组中预计算的值设置位置。最初,我们设置半径为0,然后我们使用过渡效果使圆形在加载效果中逐渐变大。

对于每个圆形,我们想要标识圆形代表的内容。这是通过在圆的中心添加文本元素来实现的。

根据我所关注的 10 个最活跃的人生成的图表如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这些人中的每一个人都发过超过 35,000 条推文。

摘要

现在你应该能够设置一个新的应用程序来查询 Twitter,使用 node.js 上的 OAuth 库创建正确的 OAuth 令牌,并建立一个气泡图。Twitter API 丰富且具有许多潜在的可视化效果。我相信我们可以提出几十个潜在的可视化效果。没有比通过实验 API 更好的学习方法了,所以不要害怕弄脏。

在下一章,我们将查看一个流行的问答网站 Stack Overflow 上的数据可视化。该 API 对大多数查询是开放的,并不需要身份验证,因此我们可能暂时不需要使用 OAuth 甚至 node.js。

第六章:Stack Overflow

2008 年,互联网上的编程问题市场被一个名为 Experts Exchange 的公司所主导。许多人对网站上文化和要求人们注册才能查看答案的规定表示不满。程序员 Jeff Atwood 和 Joel Spolsky 推出了“问答”网站 Stack Overflow。自那时以来,该网站迅速起飞,快速发展成为互联网上前 100 个网站之一。用户可以在网站上关于各种编程主题提问和回答问题。回答一个问题很好或者提出一个经过深思熟虑的问题可以赢得声誉点,这些点数突出显示。尽管 Stack Overflow 不是一个像 Facebook 和 Twitter 那样的社交媒体网站,但该网站的内容全部由用户创建和用户审核。Stack Overflow 提供了一个 API,你可以通过该 API 查询各种有趣的信息。

身份验证

查询 API 的大部分功能不需要进行身份验证。然而,如果你想要关于用户的私人信息或者想要向网站写入内容,那么你就需要进行身份验证。对于经过身份验证的应用程序,也有一个请求限制的请求限制。没有进行身份验证的情况下,一个 IP 地址每天只能限制 300 个请求。有了经过身份验证的应用程序,这个限制提高到 10,000 个请求。

提示

速率限制

许多社交媒体网站在其 API 中使用速率限制。这些限制是为了防止你使网站过载,也是为了让你不要获取太多数据。Twitter 每秒处理超过 4,000 条推文。如果你要处理它们所有人,而没有做特殊的准备,你的基础设施很快就会被压垮。

再次说明,这个网站利用OAuth来授权用户。然而,他们使用的是 OAuth 2.0,这比我们上一章使用的 OAuth 1.0a 要简单得多。我们将限制自己只使用公共信息来避免身份验证。如果你希望进行身份验证,我保证这比 Twitter 容易。你可以在api.stackexchange.com/docs/authentication.找到说明。Stack Overflow 使用与 Facebook 相同的授权系统,所以上一章的 OAuth 示例应该完全适用。

创建可视化

许多在 Stack Overflow 上的问题都有很多答案。该网站并没有优化显示最新的答案;答案是按照被接受最多的答案然后随机排序的。这样做是为了让所有的答案都有机会出现在顶部,理论上应该能鼓励人们为最好的答案投票,而不仅仅是第一个显示的答案。

对于这个可视化,我希望能展示一个问题是如何随时间被回答的。最近的答案得分更高吗?第一个答案总是最好的吗?

让我们先拉取一个有大量答案的单个问题的数据。为此,我们将使用问题 API。所有 API 端点都托管在api.stackexchange.com。我们将使用最新的 API,即版本 2.1。这也编码在 URI 中,以及特定的端点和 ID。在问题 API 中,我们感兴趣的是答案,因此我们可以针对它们进行查询,得到一个 URI:api.stackexchange.com/2.1/questions/{id}/answers

在查询字符串中,我们将指定我们要查询的站点。Stack Exchange 托管了数十个模仿 Stack Overflow 的问题和答案站点,所有这些站点都使用相同的 API 端点,因此有必要通过传递site=stackoverflow来过滤出 Stack Overflow。

function retrieveQuestionAnswers(id){
  var page = 1;
  var has_more = true;
  var results = [];
  while(has_more) {
    $.ajax(https://api.stackexchange.com/2.1/questions/ + id + "/answers?site=stackoverflow&page=" + page,{ 
        success: function(json){
          has_more = json.has_more;
          results = results.concat(json.items);},
        failure: function() { 
          has_more = false;},
          async: false
        });
        page++;
    }
  return results;
}

Twitter 为我们提供了继续令牌,我们可以将其回传给 Twitter 以请求下一页的数据。Stack Overflow 采取了不同的方法,分配了页码,使我们能够轻松地浏览结果。每个 API 调用响应中嵌入了一个名为has_more的令牌,当有符合当前查询的更多数据页时,该令牌为真。

在这个代码中,我们利用继续令牌和页码执行尽可能多的查询,以获取所有答案。我们使用了 jQuery 函数ajax,而不是更常见的getJson函数,因为我们希望同步获取数据。我们这样做是因为我们希望一次性获取整个数据集。如果您的可视化允许动态添加数据,那么您可以放宽async:false的要求。

返回的是一个对象数组,每个对象都代表一个问题的答案。如果我们给retrieveQuestionAnswers方法一个 ID,比如901115,那么我们会得到一个包含 50 个答案的数组。这些答案在两个请求中返回,上述代码将它们合并到结果数组中并返回。

每个Answer包含多个字段。返回默认字段的列表可以在api.stackexchange.com/docs/types/answer找到。对于我们的人来说,我们最感兴趣的是答案最初是在什么时候提出的,它的得分,以及它是否被选为接受的答案。这些信息可以在字段creation_datescoreis_accepted中找到。现在我们忽略其他字段。

现在我们已经有一些基本数据,可以开始考虑可视化了。我们试图传达问题的年龄和其得分之间的关系。这听起来像是一个散点图的用途。数据点本身就可以放置在日期和分数的两个轴上。在我开始这项工作之前,我的理论是:越老的问题往往会得分更高,因为它们存在的时间更长,有更多的时间积累分数。人们倾向于认为上升的数字是积极的,所以让我们利用这一点,将分数对年龄绘制成图表,如果我的理论成立,那么右侧的值将会更高。

当然,散点图很无聊,我们完全可以在 Excel 之外生成它。我们将添加一些交互性,但首先,我们仍然需要一个简单的散点图。

这可以通过几个刻度和一些圆圈轻松实现,如下面的代码所示:

var graph = d3.select("#graph");
var axisWidth = 50;
var graphWidth = graph.attr("width");
var graphHeight = graph.attr("height");
var xScale = d3.scale.linear()
  .domain([0, d3.max(data, function(item){ return item.age;})])
  .range([axisWidth,graphWidth-axisWidth]);
var yScale = d3.scale.log()
  .domain([d3.max(data, function(item){return item.score;}),1])
  .range([axisWidth,graphHeight-axisWidth]);

这会生成一个非常平缓的图表,其中大部分数据接近于零,而刻度则被一个得分超过 2000 的高异常值所扭曲,如图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这可以通过使用对数刻度来改善。每次使用像对数这样的非标准刻度时,您都会想要添加轴标签,以防止引起混淆或误导可视化的消费者。

var yAxis = d3.svg.axis()
  .scale(yScale)
  .orient('left')
  .tickValues([1,5,10,50,100,500,1000,2000])
  .tickFormat(function(item){return item;});
graph.append("g")
  .attr("transform", "translate(" + axisWidth +",0)")
  .call(yAxis);
graph.append("text")
  .attr("x", "0")
  .attr("y", graphHeight/2)
  .attr("transform", "rotate(90, 0, " + graphHeight/2 + ")")
  .text("Score");

这个图表中的标签是手动分配的,以给出最好的分散效果。您可以自动分配标签,但我发现它们被声明在奇怪的地方。我还定义了一个函数来格式化标签,否则它们倾向于使用科学记数法(2 * 10³)。最后,我附加了一些文本作为轴标签。我还添加了一个年龄轴,列出了答案的年龄(以天为单位)。

var xAxis = d3.svg.axis().scale(xScale).orient('bottom');
graph.append("g")
  .attr("transform", "translate(0," + (graph.attr("height") - axisWidth)  +")")
  .call(xAxis);
graph.append("text")
  .attr("x", graphWidth/2)
  .attr("y", graphHeight-5)
  .style("text-anchor", "middle")
  .text("Age in days");

这段代码中唯一值得注意的特殊之处在于,标签是通过变换旋转的,因为它沿着垂直轴出现。结果图表看起来像这个图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们已经有了基本的可视化,可以开始通过一些交互来使其更加完善。

我们可以添加的最简单的交互是将一个标签在有人将鼠标指针悬停在一个点上时弹出。

这可以通过使用d3on()函数来实现。这个函数可以将事件监听器绑定到 SVG 中创建的元素上。首先,我们在上面的圆圈附加代码的末尾添加如下代码:

//append circle
.on("mouseover", function(item){
  showTip(item);
});

在这里,每当用户将鼠标悬停在上面的图表中的一个圆圈上时,showTip()函数就会被调用。事件处理程序中传递的item参数是附着在悬停圆圈上的数据集合中的项目。如果您需要关于事件的额外信息,我们需要,那么可以在全局变量d3.event中找到。

在事件处理程序中,我们首先通过确保所有其他圆圈都是黑色并把选中的圆圈变成蓝色来突出选中的圆圈:

function showTip(item){
  d3.selectAll(".score").attr("fill", "black");
  d3.select(d3.event.srcElement).attr("fill", "blue");

更改圆的大小也可能很有用,以吸引更多的注意力。这可以通过简单地更新其属性来实现。接下来,我们隐藏先前的提示,并将提示内的内容设置为从选中的数据元素中获取值:

  d3.select("#tip").style("opacity", 0);
  d3.select("#count").text(item.score);
  d3.select("#age").text(Math.floor(item.age));
  d3.select("#profileImage").attr("src",
  item.owner.profile_image);
  d3.select("#profileName").text(item.owner.display_name);

最后,我们将工具提示移动到圆旁边,并让它渐显:

  d3.select("#tip").style("left", d3.event.x + "px");
  d3.select("#tip").style("top", d3.event.y + "px");
  d3.select("#tip").transition().duration(400).style("opacity", .75);
}

以下是类似以下的图表结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

将交互性添加到您的可视化中,可以使您呈现比正常情况下更多的数据。隐藏数据,以便只有在移动鼠标或单击它时才能看到,可以防止让您的用户感到不知所措,同时仍提供最多的信息。

过滤器

我们的查询返回的数据并不是我们想要的确切数据。例如,我们不在乎last_edit_date甚至是last_activity_date,但我们关心的是赞成和反对票的数量。通过拉回额外的数据,我们正在浪费带宽,并减慢用户的可视化速度。幸运的是,Stack Overflow 有一个解决方案,那就是过滤器。

提示

深度查询

如果您发现需要比 API 提供的更深入地探索 Stack Overflow 数据,您可以从www.clearbits.net/creators/146-stack-exchange-data-dump下载整个网站的数据转储。这个转储每三个月提供一次,目前压缩后为 13.4 GB。有了这个转储,您可以在不担心达到速率限制的情况下运行更复杂的查询。

过滤器控制从 API 返回的数据,可以用来添加或删除字段。它们是静态创建的,所以您只需要创建一次,您不需要在每次查询网站时,甚至每次应用程序启动时都创建一个新的过滤器。实际上,我实际上利用了 Stack Exchange 提供的 API 浏览器来提前创建我的过滤器。创建过滤器的 URL 是api.stackexchange.com/docs/create-filter

包括字段中,您可以放置一个分号来包括由分隔符指定的名称系列。属于答案对象的所有内容都以前缀 answer,所以答案所有者被称为answer.owner。默认过滤器非常包容,因此作为基本过滤器,我使用了特殊的none过滤器。这个过滤器除非明确包括,否则不包含任何字段。将none过滤器作为基础是减少过量查询的最佳实践,如图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果你从none过滤器开始,请确保在包含列表中添加令牌.items.has_more。没有项目,项目集合(根据查询持有问题、答案或用户)不包括在内,并且需要has_more来判断是否有其他页面。对我们来说,以下过滤器是完美的:

answer.answer_id;answer.owner;answer.score;answer.down_vote_count;answer.upvote_count;answer.creation_date;shallow_user.profile_image;shallow_user.display_name;.items;.has_more

create过滤器返回一个字母数字字符串,然后可以在我们的查询中使用以适当地过滤它。我们查询的 URL 如下:

"https://api.stackexchange.com/2.1/questions/" + id + "/answers?site=stackoverflow&filter=!2BjddbKa0El(rE-eV_QT8)5M&page=" + page

通过使用过滤器,我能够将 API 返回的负载从 22kB 减少到 3kB。这对于低带宽连接尤其节省大量数据。

摘要

现在你应该能够使用 Stack Exchange API 不仅查询 Stack Overflow,还可以查询所有 Stack Exchange 网站。你也应该对如何使用d3为你的可视化添加交互性有一些了解。在下一章,我们将探讨如何使用 Facebook 作为数据来源进行可视化。

第七章:Facebook

Facebook 是社交媒体世界的“重量级”选手,字面上是在大学宿舍里创建的,如今已经增长到拥有 11 亿活跃用户。地球上每七个人中就有一个。其影响力不言而喻。关于使用社交媒体 API 的任何书籍,如果没有调查如何使用 Facebook 的 API,那就称不上完整。

创建应用程序

正如你可能预料到的,对于这样一个大型的网站,有许多 API 可供构建与 Facebook 相关的应用程序使用。最简单的涉及到在网站或移动应用中集成“发布到 Facebook”按钮,而最复杂的则允许你实际上在 Facebook 的服务器上运行代码作为应用程序。我们将使用 Graph API。

Graph API 提供了一种基于 HTTP 的方法来访问信息,Facebook 称之为社交图谱。这个图谱实际上就是各种用户及其数据之间的关系。它是一个图,也是一系列节点和边的集合,而不是条形图式的图表。

为了开始,我们将向 Facebook 注册一个应用程序,就像我们之前用 Twitter 做的那样,如果我们想使用认证,还必须与 Stack Overflow 这样做。为此,我们将前往developers.facebook.com,并在顶部菜单栏中点击Apps链接。从那里,点击创建新应用。接下来,你会看到创建新应用对话框,如下面的截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

应用程序名称可以是任何你喜欢的东西,应用程序命名空间用于在 Facebook 上为你的应用程序提供一个位置,比如apps.facebook.com/NiftyVisualization。对于我们来说,这基本上是不必要的。应用程序类别完全由你决定,应该根据你要可视化的是什么来确定。Heroku是与 Facebook 合作的基于云的主机提供商,为 Facebook 应用程序提供主机空间。如果你还没有主机,Heroku 是一个合理的替代品,并且支持 node.js;然而,使用它超出了本书的范围。

一旦你填写了应用程序的详细信息,你将被要求通过解决一个 CAPTCHA 谜题来确认你是一个人。然后你会被带到编辑页面,在那里你可以填写最后几个细节然后再测试一下你对 API 的访问。它看起来如下面的截图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在此处,你需要至少填写一个应用域名。这个值在你登录时由 API 检查,以确保你的应用是从一个授权的域名中使用的。不幸的是,你不能从不是从域名中服务的文件访问 Facebook 的 Graph API。这意味着仅仅访问file://c:/code/visualization.html并不能让你访问 Facebook 的 API。幸运的是,并非一切都失去了,使用localhost是被允许的,但这意味着我们必须运行一个 HTTP 服务器。我们可以利用在前几章中使用过的相同的Node.js安装。

带有 Facebook 登录的网站 URL 应设置为你的 OAuth 密钥交换的返回 URL。实际上我们可以将其设置为任何内容,因为我们打算使用 AJAX 进行身份验证,我们的用户实际上永远不会离开我们的初始页面。

使用 API

完全可以通过手动执行 OAuth 来与 Facebook 认证和授权你的可视化。然而,Facebook 慷慨地提供了一个非常易用的 JavaScript SDK。API 将登录过程抽象为函数调用。为了使用 API,我们首先需要在我们的可视化中包含它。为此,只需在你的一个script标签内包含以下脚本:

(function(d){
  var js;
  var id = 'facebook-jssdk';
  var ref = d.getElementsByTagName('script')[0];
  if (d.getElementById(id)) {return;}
  js = d.createElement('script'); js.id = id; js.async = true;
  js.src = "//connect.facebook.net/en_US/all.js";
  ref.parentNode.insertBefore(js, ref);
}(document));

这段代码将在你的文档中创建一个新的script标签,并将其源设置为 Facebook 网站上的一文件。添加这样的script标签将导致浏览器加载该脚本文件的内容并执行它。因为我们以异步方式加载脚本,我们需要等待它加载完毕才能使用它。SDK 在初始化后调用一个钩子,fbAsyncInit。我们只需要将一个函数与该钩子相关联,如下面的代码所示:

window.fbAsyncInit = function() {
  FB.init({
  appId      : '525498574499442', 
  channelUrl : '//localhost:8080/channel', 
  status     : true, 
  cookie     : true, 
  xfbml      : true  // parse XFBML
  });
};

这将为 SDK 提供从开发者网页上可用的 App ID。另外,在这里我提供了channelUrl,它用于解决某些浏览器上出现的跨域问题。设置状态将使init方法获取status的值。cookie将启用 cookie 支持。最后,xfbml启用了 Facebook 标记语言。那是什么?它是由 Facebook SDK 控制的特殊格式的 HTML 元素的集合。

例如,如果我们想要显示一个登录按钮(多么方便,我们确实想要显示一个登录按钮)那么我们可以简单地添加以下代码:

<fb:login-button show-faces="true" width="200" max-rows="1"scope="user_birthday,email,friends_birthday">
</fb:login-button>

当未认证的用户打开页面时,会显示一个登录按钮。当认证用户访问页面时,会显示他们自己的登录信息。你会注意到scope属性;这个属性用于向 Facebook 提供一个权限列表,你请求这些权限。在这里,我们请求了登录用户的生日、电子邮件 ID 以及他们朋友的生日。在登录时,用户会被 Facebook 提示允许你的可视化访问这些权限。大约有三十多种不同的权限可以从 Facebook 请求,这些权限涵盖了从获取登录用户的信息,到他们的朋友、事件和回复确认单的一切。在这里仔细查看以发现有趣的可视化方面是非常值得的。

认证拼图的最后一步是提供一个函数,让登录按钮在登录用户后调用:

FB.Event.subscribe('auth.authResponseChange', function(response) {
  if (response.status === 'connected') {
    //use SDK here
  } else if (response.status === 'not_authorized') {//not authorized
    FB.login({scope: "user_birthday,email,friends_birthday"});
  } else { //not logged in
    FB.login({scope: "user_birthday,email,friends_birthday"});
  }
});

此事件在授权响应发生更改时触发,例如当我们从登录按钮获得认证时。

获取数据

在我们获取数据之前,我们可能应该决定我们想可视化哪些数据。关于登录用户的数据并不多(至少对我来说是这样,但我几乎不使用 Facebook)。这让我们转向了查看我们的朋友。我发现我的朋友们使用来访问 Facebook 的设备非常有趣。他们是更多的安卓用户还是 iOS 用户?这些信息作为朋友集合的一部分是可用的。为了获取这些信息,我们可以使用FB.api()方法:

FB.api('/me?fields=friends.fields(devices)', function(response){
  for(i = 0; len = response.friends.data.length; i< len; i++){
    var friend = response.friends.data[i];
    if(friend.devices)
      for(j = 0; j< friend.devices.length; j++)
    if(friend.devices[j].hardware != "iPad")
      operatingSystems[friend.devices[j].os]++;
  }
});

我们将一个要检索的 URL 传递给api()方法。在这个例子中,我们请求特殊的 URL/me,它指的是当前登录的用户。我们还提供一个过滤器,以便只检索朋友集合,实际上,只为每个朋友检索设备的集合。在回调中,我们只是在计算安卓和 iOS 设备的数量。Facebook 将 iPad 和 iPhone 视为不同的设备,但我们不想将 iOS 作为一种访问方法重复计算,所以忽略任何 iPad。一旦执行此代码,我们最终会得到一个设备计数集合。对于我的朋友们,我得到了以下结果:

{Android: 28, iOS: 36}

可视化

一种有效的可视化技术是通过显示不同类别的相对强度来显示一个缩放图像。我们在使用气泡的 Twitter 章节中看到了这种技术的应用。我们可以通过使用图像而不是仅仅使用圆形来进一步发挥这种技术。

第一步是找到已经是 SVG 的 Android 和 iOS 标志。结果证明,维基百科是这一的好来源,他们的图片都根据创意共享许可发布,这意味着我们可以在我们的可视化中使用它们。SVG 的一个非常出色的功能是,你可以很容易地通过使用定义将两张图片合并在一起。如果你打开一个像upload.wikimedia.org/wikipedia/commons/e/e1/Android_dance.svg这样的安卓标志的 SVG,你可以复制另一个图片下的<defs>标签下的所有标记。我将安卓和苹果标志移动到我的源标记中。如果我想要显示它们,我可以使用<use>标签并参考通过 ID 定义的引用。代码如下:

<defs>
  <g id="appleLogo">
    <!--various shapes needed to build the Apple logo-->
  </g>
  <g id="androidLogo">
    <!--various shapes needed to build the Android logo-->
  </g>

</defs>
<use x="0" y="10" xlink:href="#appleLogo"/>
<use x="512" y="10" xlink:href="#androidLogo" />

这将在我们的 SVG 中创建一个苹果标志旁边的一个安卓标志。我们知道我们可以利用d3来构建和缩放标志,运气好的是,我们有的两个 SVG 都是 256px 正方形,所以在我们将它们翻译之前,它们看起来大小大致相同。d3相对简单,如下面的代码所示:

var visualization = d3.select("#visualization");
visualization.selectAll(".logo").data(operatingSystems)
.enter().append("use")
.attr("xlink:href", function(item){ return "#" + item.os + "Logo";})
.attr("transform", function(item, index){
  return "translate("  + 300 * index + " 0),scale(" + (item.users / operatingSystems[0].users) + ")";
});

我们首先选择 SVG,然后不是附加形状,而是使用声明进行附加。xlink:href属性取定义的值以包括在内。接下来,我们将标志缩放和移动,使它们紧挨在一起并具有适当的尺寸。我们将第一个标志设置为基线尺寸,此后每个标志都以该尺寸的百分比绘制。这之所以有效,是因为我们的数字相当接近。如果数字差异很大,就需要更健壮的策略。添加一些额外的文本元素后,结果是以下图表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

摘要

您现在应该已经掌握了如何使用 Facebook API 检索数据的方法。此数据然后可以使用任何技术进行可视化。在下一章中,我们将查看新兴的 Google+社交网络,并了解我们如何可以利用那里存在的数据进行可视化。

第八章:Google+

在主要的社交网站中,Google+是最新的成员。虽然是个新手,但它确实拥有庞大的用户基础,声称拥有超过 3.5 亿活跃账户(ca.ign.com/articles/2013/05/02/report-google-bigger-than-twitter-with-359-million-active-users)。这并非 Google 首次尝试打入价值十亿美元的社会媒体市场。他们过去曾创建过 Google Buzz、Google Friend Connect 和 Orkut,试图获得大量用户基础。除 Orkut 外都已停止使用,其用户基础几乎全部位于巴西。Google 故意没有创建写入 API,希望这能消除自动发布的垃圾信息。Google+提供了一个只读 API,我们可以利用它来创建可视化;然而,与其他此类 API 相比,该 API 非常有限——在本书撰写时,你甚至不能列出一个圈子的成员。

创建一个应用

Google+是另一个 OAuth 2.0 网站,因此我们当然需要获取一个应用程序密钥作为创建任何可视化的第一步。这也意味着我们将需要一个返回 URL,因此我们再次需要设置一个 HTTP 服务器来运行可视化。

第一步是使用你的 Google 账户登录code.google.com/apis/console。如果你没有这样的账户,你也可以从那个页面创建一个。一旦到达该网站,你将看到一个巨大的按钮,允许你创建一个应用程序项目。这个控制台实际上管理着 Google 所有 API 的访问权限,而且有很多。

接下来,你将看到一个各种 API 的巨大列表。如果你向下滚动很久,最终会找到 Google+(使用搜索功能,这将节省你数小时的滚动时间)。将开关切换到“开启”位置。你可能需要同意几个用户协议。确保像往常一样阅读整个协议。

下一步是请求一个新的密钥,如下面的截图所示。这可以在API 访问标签中完成,在此标签中你应该点击创建 OAuth 2.0 客户端 ID…。在打开的对话框中,你需要填写一个应用程序名称和一个 URL。这不是 OAuth 交换的 URL;那在下一个标签里。在这个标签中,输入一个 URL,OAuth 请求可能从中发起并返回。对于我们来说,http://localhost:8080将是域名:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后我们将收到几个供我们应用程序使用的密钥。客户端 ID 是你想在脚本中使用的字段。

检索数据

和 Facebook 一样,我们可以针对 OAuth 2.0 端点进行手动认证,但让我们使用 Google 提供的 API。连接起来非常简单:

document.addEventListener("DOMContentLoaded",
 function() {
    var po = document.createElement('script');
    po.type = 'text/javascript'; po.async = true;
    po.src = 'https://plus.google.com/js/client:plusone.js';
    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(po, s);
  });

当文档准备就绪时,这将运行并从 Google 服务器加载 API 脚本,通过在页面中插入一个新的script标签,该标签位于包含 jQuery 的标签之前。加载的文档包含一组可以与 Google API 交互的 JavaScript 函数,但不是特定于 Google+ API——这发生在登录之后。

要添加一个登录按钮,我们添加以下 HTML 代码:

<button class="g-signin"
   data-scope="https://www.googleapis.com/auth/plus.login"
   data-requestvisibleactions="http://schemas.google.com/AddActivity"
   data-clientId="988552574579.apps.googleusercontent.com"
   data-callback="onSignInCallback"
   data-theme="dark"
   data-cookiepolicy="single_host_origin">
</button>

这将生成登录按钮。这个按钮上附着的各种data-属性由我们从 Google 加载的脚本处理。登录是对 Google+而不是其他 Google API 的。客户端 ID 应设置为创建应用时获取的 ID。最重要的是,指派了一个回调函数,当登录请求成功时将被激活。回调将动态加载 Google+ API:

function onSignInCallback(authResult) {
      gapi.client.load('plus','v1', function(){
        if (authResult['access_token']) {
          $('#gConnect').hide();
 retrieveFriends();
        } else if (authResult['error']) {
          console.log('There was an error: ' + authResult['error']);
          $('#gConnect').show();
        }
        console.log('authResult', authResult);
      });
    }

一旦加载了 Google+的 API,我们就可以利用它,正如我们在高亮行所做的那样。这个函数还隐藏了登录按钮,所以用户不会尝试多次登录。retreiveFriends简单且只会发送一个请求以获取朋友列表:

retrieveFriends: function(){
      var request = gapi.client.plus.people.list({
        'userId': 'me',
        'collection': 'visible'
      });
      request.execute(retrieveFriendsCallback);
    }

现在我们有了朋友列表,我们可以开始使用它们来构建一个简单的可视化。

可视化

d3故意避开了提供具体的可视化。没有一个函数你可以调用以得到条形图或散点图。相反,它提供了创建可视化的工具;这些工具进而提供高度的灵活性,并赋予创建独特可视化的能力。其中较为强大的工具之一是布局机制。布局提供了一些必须编写以实现某种可视化的样板代码。

我们将使用力导向图布局。力导向图提供了一种可视化相互连接数据的方法。节点之间联系的强度通常是一个关于节点之间关系密切程度的函数。

我们的第一步是将我们的数据转换为节点和边的列表。由于 API 返回的数据非常有限,我们只能建立你和你朋友之间的关系。这些关系将构成边或链接,而朋友则是节点:

var nodes = [];
 var links = [];
var centerNode = { name: "Me"};
nodes.push(centerNode);
for(i = 0; i< data.items.length; i++){
   var node = { name: data.items[i].displayName, image: data.items[i].image.url};
   nodes.push(node);
   links.push({source: centerNode, target: node});
}

现在我们有了节点和链接,我们可以使用它们来创建一个力布局:

var graph = d3.select("#graph");
var force = d3.layout.force().charge(-120).linkDistance(100).size([500,500]).nodes(nodes).start();

chargelinkDistance函数控制节点分散自己的范围。对于链接,我们绘制一条简单的线来表示它们:

var link = graph.selectAll(".link").data(links).enter().append("line").attr("class", "link");

节点有点复杂,因为对于每一个我们需要从 Google+数据中设置一个图片,初始位置以及大小。我们还需要给节点附加一个事件处理程序,这样当拖动时,force.drag动作会被触发:

var node = graph.selectAll(".node").data(nodes)
           .enter()
            .append("image")
             .attr("xlink:href", function(d){ return d.image;})
            .attr("class", "node")
            .attr("r", 15)
            .attr("x", 250)
            .attr("y", 250)
            .attr("width", 50)
            .attr("height", 50)
            .call(force.drag);

最后,我们需要指示d3在动画化图形时对每个刻度采取什么行动:

force.on("tick", function () {calculatePosition(link, node);});

这将生成一个图表,显示我在 Google+上与朋友的链接,如下面的屏幕截图所示。如果你点击并拖动一个节点,它将会移动,所有节点都会重新平衡自己以适应这种移动:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

摘要

Google+的limited API 确实限制了我们可以创建的一些可视化。多年来一直有传言称 Google 将在 Google+中提供额外的功能,但到目前为止我还没有看到任何真正的行动。你现在应该能够对 Google+进行身份验证并从中获取数据。你也应该能够使用d3提供的图形美观的力导向布局以及其他可用的布局。

转载请注明出处或者链接地址:https://www.qianduange.cn//article/14940.html
标签
golang
评论
发布的文章

安装Nodejs后,npm无法使用

2024-11-30 11:11:38

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!