R 和 JavaScript 高级数据可视化(三)

原文:Pro Data Visualization Using R and JavaScript

协议:CC BY-NC-SA 4.0

七、条形图

第六章探讨了如何使用时序图来查看一段时间内的缺陷数据,这一章介绍了条形图,它显示了相对于特定数据集的有序或分级数据。它们通常由 x 轴和 y 轴组成,并有条形或彩色矩形来表示类别的值。

威廉·普莱费尔(William Playfair)在 1786 年的第一版《商业与政治地图集》(The Commercial and Political Atlas)中创建了条形图,以显示苏格兰与世界不同地区的进出口数据(见图 7-1 )。他出于需要创造了它;地图册中的其他图表是时间序列图表,展示了数百年的贸易数据,但对于苏格兰来说,只有一年的数据。在使用时间序列图时,Playfair 认为这是一种低劣的可视化;一种与现有资源的妥协,因为它“不包含时间的任何部分,并且在效用上比包含时间的部分差得多”(Playfair,1786,第 101 页)。

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

图 7-1

威廉·普莱费尔的条形图显示了苏格兰的进出口数据

Playfair 最初对他的发明评价很低,以至于他都懒得把它收录到随后的第二版和第三版地图集里。他继续设想一种不同的方式来展示整体的一部分;为此,他在 1801 年出版的统计年鉴中发明了饼状图。

条形图是展示分级数据的好方法,不仅因为条形图是显示数值差异的清晰方式,而且通过使用不同类型的条形图(如堆积条形图和分组条形图),该模式还可以扩展为包括更多的数据点。

标准条形图

让我们来看看您已经熟悉的数据——上一章的bugsBySeverity数据:

head(bugsBySeverity)

          Blocker Minor Moderate
  1/11/21       0     1        0
  1/12/20       0     1        0
  1/12/21       1     2        0
  1/13/20       1     0        0
  1/17/21       2     0        0
  1/18/21       0     0        1

您可以创建一个新的列表,其中包含每种错误类型的总和,并以条形图的形式显示总数,如下所示:

totalBugsBySeverity <- c(sum(bugsBySeverity[,1]), sum(bugsBySeverity[,2]), sum(bugsBySeverity[,3]))
barplot(totalBugsBySeverity, main="Total Bugs by Severity")
axis(1, at=1: length(totalBugsBySeverity), lab=c("Blocker", "Critical", "Minor"))

该代码生成如图 7-2 所示的图表。

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

图 7-2

按严重性划分的错误条形图

堆积条形图

堆积条形图允许我们显示类别中的子部分或分段。假设您使用bugsBySeverity时间序列数据,并希望查看每天新出现的 bug 的危险程度:

t(bugsBySeverity)

    1/11/21 1/12/20 1/12/21 1/13/20 1/17/21 1/18/21 1/2/21 1/21/20 1/22/20
Blocker   0       0       1       1       2       0      0       1       1
Minor     1       1       2       0       0       0      1       0       0
Moderate  0       0       0       0       0       1      0       0       0

    1/24/20 1/24/21 1/25/20 1/27/21 1/29/21 1/3/20 1/4/20 1/5/20 1/5/21
Blocker   0       0       1       0       0      0      0      0      0
Minor     1       1       0       1       0      1      1      1      1
Moderate  0       0       0       0       1      0      0      0      0

    1/9/20 10/1/20 10/10/20 10/15/20 10/16/20 10/18/20 10/21/20 10/25/20
Blocker  1       0        0        1        0        0        0        1
Minor    0       1        0        0        1        0        1        0
Moderate 0       0        1        0        0        2        1        0

    10/26/20 10/29/20 10/30/20 10/6/20 11/17/20 11/18/20 11/19/20 11/21/20
Blocker    0        1        0       0        0        1        0        0
Minor      0        0        1       1        1        0        1        1
Moderate   1        1        0       0        0        0        0        0

    11/23/20 11/26/20 11/4/20 11/8/20 12/14/20 12/15/20 12/17/20 12/21/20
Blocker    0        2       1       1        1        1        0        1
Minor      1        0       1       0        0        0        1        0
Moderate   0        0       0       0        1        0        0        0

    12/22/20 12/23/20 12/24/20 12/27/20 12/29/20 12/3/20 12/31/20 2/12/21
Blocker    1        0        1        0        0       1        0       1
Minor      0        1        0        0        1       0        1       0
Moderate   1        0        0        1        0       0        0       0

    2/13/21 2/14/20 2/15/20 2/15/21 2/16/20 2/22/21 2/24/20 2/25/21
Blocker   0       1       0       1       1       1       1       0
Minor     0       0       1       0       0       1       0       1
Moderate  1       0       0       0       0       0       0       0

    2/26/21 2/28/21 2/3/21 2/4/21 2/8/21 3/1/20 3/1/21 3/11/21 3/14/21
Blocker   1       1      1      1      1      0      1       2       0
Minor     1       0      0      0      0      0      0       1       1
Moderate  0       0      0      0      0      2      0       0       0

    3/17/21 3/2/20 3/2/21 3/22/20 3/23/21 3/24/20 3/25/21 3/26/20 3/28/20
Blocker   1      1      1       1       0       0       1       0       1
Minor     0      0      0       1       1       0       0       1       0
Moderate  0      0      0       0       0       1       0       0       0

    3/3/21 3/31/20 3/31/21 3/6/21 3/7/20 3/7/21 4/12/21 4/13/20 4/15/21
Blocker  1       0       1      1      0      0       0       0       0
Minor    0       0       0      0      0      0       0       1       0
Moderate 0       1       0      0      1      1       1       0       1

    4/18/21 4/19/21 4/20/20 4/25/20 4/26/21 4/27/20 4/29/21 4/4/20 4/5/21
Blocker   0       0       1       0       1       1       1      0      2
Minor     2       1       0       1       0       0       0      1      1
Moderate  0       0       0       0       0       0       0      0      0

    4/7/20 4/8/20 5/1/20 5/10/20 5/11/21 5/12/20 5/14/21 5/16/21 5/17/20
Blocker  1      1      2       0       1       1       0       1       1
Minor    0      0      0       1       0       1       1       0       0
Moderate 0      1      0       0       0       0       0       0       0

    5/17/21 5/2/21 5/20/20 5/20/21 5/22/20 5/24/21 5/25/20 5/26/21 5/27/20
Blocker   1      1       0       1       2       0       0       1       1
Minor     0      0       0       0       0       1       0       0       0
Moderate  0      0       1       1       0       0       1       0       0

    5/27/21 5/28/20 5/28/21 5/29/21 5/30/20 5/31/20 5/6/20 5/8/20 6/11/20
Blocker  1       0       1       2       1       1      0      1       1
Minor    0       1       0       0       0       0      1      0       0
Moderate 0       0       0       0       0       0      0      0       0

    6/11/21 6/14/20 6/16/21 6/2/21 6/20/20 6/28/20 6/3/20 6/3/21 6/4/20
Blocker   1       1       2      1       1       1      0      1      0
Minor     0       0       0      0       0       0      0      0      1
Moderate  0       0       0      0       0       0      1      0      0

    6/4/21 6/6/21 6/7/20 6/7/21 6/8/21 6/9/21 7/14/20 7/18/20 7/2/20
Blocker  0      1      0      1      0      0       1       2      0
Minor    1      0      1      0      1      1       0       0      1
Moderate 0      0      1      0      0      0       0       0      0

    7/22/20 7/23/20 7/25/20 7/28/20 7/29/20 7/9/20 8/10/20 8/17/20 8/2/20
Blocker   1       0       0       1       0      0       0       0      0
Minor     0       1       0       0       1      1       1       0      1
Moderate  0       0       1       0       0      0       0       2      0

    8/21/20 8/22/20 8/23/20 8/24/20 8/26/20 8/27/20 8/28/20 8/29/20 8/3/20
Blocker   1       0       0       2       1       0       0       1      0
Minor     0       0       1       0       0       1       1       0      1
Moderate  0       1       0       0       0       0       0       0      0

    8/6/20 9/10/20 9/11/20 9/14/20 9/16/20 9/2/20 9/21/20 9/8/20
Blocker  1       1       1       0       0      0       0      0
Minor    0       0       0       0       0      1       1      0
Moderate 0       0       0       1       1      0       0      1

您可以用堆积条形图表示以下数据,如图 7-3 所示:

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

图 7-3

按严重性和日期排列的错误堆积条形图。因为每天臭虫的总数不同,所以这些条的高度也不一样

barplot(t(bugsBySeverity), col=c("#CCCCCC", "#666666", "#AAAAAA"))
legend("topleft", inset=.01, title="Legend", c("Blocker", "Criticals", "Minors"), fill=c("#CCCCCC", "#666666", "#AAAAAA"))

总的缺陷由条形的全高表示,每个条形的彩色部分表示缺陷的严重程度。堆积条形图使我们能够显示数据中的细微差别,尽管人们可能希望在可视化时减少日期的数量以获得更清晰的图片。

分组条形图

分组条形图使我们能够显示与堆叠条形图相同的细微差别,但我们不是将各段放在彼此的顶部,而是将它们分成并排的分组。图 7-4 显示 x 轴上的每个日期都有三个与之相关的条形,每个条形代表一个关键程度类别:

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

图 7-4

按严重性和日期分组的错误条形图

barplot(t(bugsBySeverity), beside=TRUE, col=c("#CCCCCC", "#666666", "#AAAAAA"))
legend("topleft", inset=.01, title="Legend", c("Blocker", "Criticals", "Minors"), fill=c("#CCCCCC", "#666666", "#AAAAAA"))

由于数据的密度,乍一看,数字 7-3 和 7-4 可能是相同的。为了避免这种情况,我们可以使用下面的代码来减少数据点的数量,只显示五天的数据。尝试使用这两个代码片段来查看更改。

barplot(t(bugsBySeverity[1:10,]), col=c("#CCCCCC", "#666666", "#AAAAAA"))
legend("topleft", inset=.01, title="Legend", c("Blocker", "Criticals", "Minors"), fill=c("#CCCCCC", "#666666", "#AAAAAA"))

versus

barplot(t(bugsBySeverity[1:10,]), beside=TRUE, col=c("#CCCCCC", "#666666", "#AAAAAA"))
legend("topleft", inset=.01, title="Legend", c("Blocker", "Criticals", "Minors"), fill=c("#CCCCCC", "#666666", "#AAAAAA"))

可视化和分析生产事故

如果您开发的产品被某个人使用——最终用户、消费服务甚至内部客户——您很可能经历过生产事故。当应用程序的某个部分在生产中对用户不正常时,就会发生生产事故。它非常像一个 bug,但它是您的客户所经历和报告的 bug。

就像 bug 一样,生产事件是正常的,是软件开发的预期结果。谈论事件时,有三个主要问题需要考虑:

  • 报告的错误的严重性或影响程度:站点中断和小的布局错误之间有很大的区别。

  • 频率,或者说事件发生或重复发生的频率:如果你的网络应用充满问题,你的客户体验、你的品牌和你正常的工作流程都会受到影响。

  • 持续时间,或个别事件持续多长时间:持续时间越长,受影响的顾客就越多,对你的品牌影响就越大。

处理生产事故是产品运营和组织成熟的重要组成部分。根据事件的严重程度,它们可能会破坏你的日常工作;团队可能需要停止一切工作,努力解决这个问题。优先级较低的项目可以排队,并与常规功能工作一起引入常规工作主体。

与处理生产事故同样重要的是能够分析生产事故的趋势,以确定问题领域。问题区域通常是生产中经常出现问题的特征或部分。一旦我们确定了问题领域,我们就可以进行根本原因分析,并有可能开始围绕这些领域构建主动的脚手架。

Note

主动脚手架是我创造的一个术语,用来描述建立故障转移或额外的安全围栏,以防止问题区域的问题再次出现。主动搭建可以是从检测用户何时接近容量限制(如浏览器 cookie 限制或应用程序堆大小,并在问题发生前纠正)到注意第三方资产的性能问题,并在将它们呈现给客户端之前拦截和优化它们。

另一种处理生产事故的有趣方式是 Heroku 过去处理事故的方式:将事故与逐月正常运行时间可视化一起放在时间线上,并公开发布。Heroku 的生产事件时间表在 https://status.heroku.com/可用;见图 7-5 。

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

图 7-5

Heroku 状态页面

GitHub 过去也有一个很棒的状态页面,可以可视化关于其性能和正常运行时间的关键指标(见图 7-6 )。具有讽刺意味的是,他们现在已经切换到 Heroku 放弃的时间线方法(见图 7-7 ,来自 www.githubstatus.com/history )。

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

图 7-7

GitHub 的时间表

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

图 7-6

GitHub 状态页

就我们的目的而言,本章使用条形图按特征查看生产事故,以开始识别我们自己产品中的问题区域。

用 R 在条形图上绘制数据

如果我们想要规划出我们的生产事件,我们必须首先获得数据的导出,就像我们需要为 bug 做的那样。因为生产事故通常是一次性事件,公司通常使用一系列方法来跟踪它们,从吉拉( www.atlassian.com/software/jira/overview )等票务系统到维护项目的电子表格,只要我们能检索到原始数据,什么都行。(乔恩在这里做了样本数据: http://jonwestfall.com/data/productionincidents.csv )。)

一旦我们有了原始数据,它可能看起来像下面这样:一个逗号分隔的平面列表,包含 ID、日期戳和描述列。还应该有一列列出发生事件的应用程序的功能或部分。

ID,DateOpened,DateClosed,Description,Feature,Severity
880373,5/22/21 10:14,5/25/21 11:52,Fwd: 2 new e-books Associate Editors,General Inquiry,1
837947,4/29/21 12:35,5/7/21 14:09,Fwd: New Resource to Post,General Inquiry,2
489036,4/23/21 14:38,4/27/21 9:00,STP ebook editor with finished book,General Inquiry,1
443617,1/25/21 17:43,1/26/21 8:49,New member - IRC Committee at STP,General Inquiry,2
911894,1/18/21 10:25,1/20/21 8:51,Fwd: Updates to International Relations Committee page,General Inquiry,1
974124,1/11/21 14:55,1/12/21 10:55,Fwd: New Resource to Post,General Inquiry,2
341352,1/2/21 10:51,1/5/21 16:26,New eBooks,eBook Publishing,1

让我们将原始数据读入 R 并存储在一个名为prodData的变量中:

> prodIncidentsFile <- "http://jonwestfall.com/data/productionincidents.csv";
> prodData <- read.table(prodIncidentsFile, sep=",", header=TRUE)
> prodData
      ID    DateOpened     DateClosed  Description              Feature           Severity
1 880373 5/22/21 10:14  5/25/21 11:52  Fwd: 2 new e-books Associate Editors    General Inquiry   1
2 837947 4/29/21 12:35   5/7/21 14:09  Fwd: New Resource to Post    General Inquiry   2
3 489036 4/23/21 14:38   4/27/21 9:00  STP ebook editor with finished book    General Inquiry   1
4 443617 1/25/21 17:43   1/26/21 8:49  New member - IRC Committee at STP    General Inquiry   2
5 911894 1/18/21 10:25   1/20/21 8:51  Fwd: Updates to International                                        Relations Committee page    General Inquiry   1
6 974124 1/11/21 14:55  1/12/21 10:55  Fwd: New Resource to Post    General Inquiry   2
7 341352  1/2/21 10:51   1/5/21 16:26  New eBooks     eBook Publishing  1

我们希望按照Feature列对它们进行分组,这样我们就可以绘制特性总数的图表。为此,我们在 R 中使用了aggregate()函数。aggregate()函数接受一个 R 对象、一个用作分组元素的列表和一个应用于分组元素的函数。因此,假设我们调用aggregate()函数,将 ID 列作为 R 对象传入,让它按Feature列分组,并让 R 获得每个特性分组的长度:

prodIncidentByFeature <- aggregate(prodData$ID, by=list(Feature=prodData$Feature), FUN=length)

这段代码创建了一个如下所示的对象:

> prodIncidentByFeature
           Feature x
1 eBook Publishing 1
2  General Inquiry 6

然后我们可以将这个对象传递给barplot()函数,得到如图 7-8 所示的图表。

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

图 7-8

开始绘制条形图

barplot(prodIncidentByFeature$x)

这是一个很好的开始,确实讲述了一个故事,但它不是很有描述性。除了 x 轴没有被标记的事实之外,问题区域由于没有对结果排序而变得模糊。

订购结果

让我们使用order()函数按照每个事件的总数对结果进行排序:

prodIncidentByFeature <- prodIncidentByFeature[order(prodIncidentByFeature$x),]

然后,我们可以通过水平分层条形图并将文本旋转 90 度来设置条形图的格式,以突出显示这种顺序。

要旋转文本,我们必须使用par()功能改变我们的图形参数。更新图形参数具有全局影响,这意味着我们在更新后创建的任何图表都会继承这些更改,因此我们需要保留当前设置,并在创建条形图后重置它们。我们将当前设置存储在一个名为opar的变量中:

opar <- par(no.readonly=TRUE)

Note

如果您在 R 命令行中跟随,前面的行本身不会生成任何东西;它只是设置图形参数。

然后,我们将新参数传递给par()调用。我们可以使用las参数来格式化轴。las参数接受以下值:

  • 0 是文本平行于轴的默认行为。

  • 1 显式使文本水平。

  • 2 使文本垂直于轴。

  • 3 显式使文本垂直。

par(las=3)

然后我们再次调用barplot(),但是这次传入参数horiz=TRUE,让 R 水平而不是垂直绘制线条:

barplot(prodIncidentByFeature$x, xlab="Number of Incidents", names.arg=prodIncidentByFeature$Featurehoriz =真,space=1, cex.axis=0.6, cex.names=0.8, main="Production Incidents by Feature", col= "#CCCCCC")

And, finally, we restore the saved settings so that future charts don't inherit this chart's settings:
> par(opar)

这段代码产生了如图 7-9 所示的可视化效果。

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

图 7-9

按功能划分的生产事故条形图

从这张图表中,你可以看到最大的问题领域是标签为一般查询的类别,其次是电子书出版。

创建堆积条形图

围绕这些功能的问题有多严重?接下来,让我们创建一个堆积条形图,查看每个生产事件的严重性细分。为此,我们必须创建一个表,在该表中,我们按特征和严重性对生产事件进行细分。我们可以为此使用table()函数,就像我们在上一章中对 bug 所做的那样:

prodIncidentByFeatureBySeverity <- table(factor(prodData$Feature),prodData$Severity)

该代码创建一个如图 7-10 所示格式的变量,其中行代表每个特性,列代表每个严重级别:

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

图 7-11

按功能和严重性划分的生产事件堆积条形图

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

图 7-10

按功能和严重性划分的生产事件堆积条形图

prodIncidentByFeatureBySeverity

                   1 2
  eBook Publishing 1 0
  General Inquiry  3 3
opar <- par(no.readonly=TRUE)
par(las=3, mar=c(5,5,5,5))
barplot(t(prodIncidentByFeatureBySeverity), xlab="Number of Incidents", names.arg=rownames(prodIncidentByFeatureBySeverity), horiz=TRUE, space=1, cex.axis=0.6, cex.names=0.8, main="Production Incidents by Feature", col=c("#CCCCCC", "#666666", "#AAAAAA", "#333333"))
legend("bottom", inset=.01, title="Legend", c("Sev1", "Sev2"), fill=c("#CCCCCC", "#666666"))
par(opar)

有意思!我们失去了排序,但那是因为我们有许多新的数据点可供选择。高级总量与此图的相关性较低;更重要的是严重性的分解。

D3 的条形图

现在,您已经知道了使用条形图在较高层次上汇总数据的好处,以及获得堆积条形图所能揭示的粒度细分的好处。让我们换个角度,使用 D3 来看看如何创建一个高级条形图,它允许我们深入每个条形图,以查看运行时数据的粒度表示。

我们首先在 D3 版本 3 中创建一个条形图,然后创建一个堆叠条形图。当我们的用户将鼠标放在条形图上时,我们将叠加堆叠的条形图,以显示数据如何实时分解。

创建垂直条形图

因为我们在第四章中已经在 D3 中制作了一个水平条形图,现在我们将制作一个垂直条形图。遵循我们在前几章中建立的相同模式,我们首先创建一个基本的 HTML 框架结构,它包括一个到 D3 版本 3 库的链接。我们使用上一章中用于正文和轴路径的相同的基本样式规则,以及一个额外的规则来将 bar 类中的所有元素着色为深灰色。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script src="d3.v3.js"></script>
<style type="text/css">
     body {
          font: 15px sans-serif;
     }
     .axis path{
          fill: none;
          stroke: #000;
          shape-rendering: crispEdges;
     }
     .bar {
          fill: #666666;
     }
</style>
</head>
<body></body>
</html>

接下来,我们创建script标签来保存所有的图表代码,以及保存尺寸信息的初始变量集:基本高度和宽度,用于 x 和 y 坐标信息的 D3 scale 对象,保存边距信息的对象,以及从总高度中去掉上下边距的调整后的高度值:

<script>
var w = 960,
    h = 500,
    x = d3.scale.ordinal().rangeRoundBands([0, w]),
    y = d3.scale.linear().range([0, h]),
    z = d3.scale.ordinal().range(["lightpink", "darkgray", "lightblue"])
    margin = {top: 20, right: 20, bottom: 30, left: 40},
    adjustedHeight = 500 - margin.top - margin.bottom;
</script>

接下来,我们创建 x 轴对象。请记住,在前面的章节中,轴还没有画出来,所以我们需要稍后在可缩放矢量图形(SVG)标记中调用它,我们将创建这个标记来画轴:

var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");

让我们将 SVG 容器绘制到页面上。这将是我们将绘制到页面上的所有其他内容的父容器。

var svg = d3.select("body").append("svg")
    .attr("width", w)
    .attr("height", h)
  .append("g")

下一步是读入数据。我们将使用与 R 示例相同的数据源:平面文件productionIncidents.txt。我们可以使用d3.csv()函数读取并解析文件。一旦文件的内容被读入,它们就被存储在变量data中,但是如果出现任何错误,我们将把错误细节存储在一个我们称之为error的变量中。

d3.csv("http://jonwestfall.com/data/productionincidents.csv", function(error, data) {
      }

在这个d3.csv()函数的范围内,我们将放置大部分剩余的功能,因为这些功能依赖于数据的处理。

让我们按特征汇总数据。为此,我们使用d3.nest()函数并将键设置为Feature列:

nested_data = d3.nest()
     .key(function(d) { return d.Feature; })
     .entries(data);

这段代码创建了一个对象数组。

在这个数组中,每个对象都有一个列出特性的键和一个列出每个生产事件的对象数组。

我们使用这个数据结构来创建核心条形图。我们为此创建了一个函数:

function barchart(){
}

在这个函数中,我们设置了svg元素的transform属性,它设置了包含将要绘制的图像的坐标。在这种情况下,我们将其限制为左边距和上边距值:

svg.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

我们还为 x 轴和 y 轴创建缩放对象。对于条形图,我们通常对 x 轴使用顺序刻度,因为它们用于离散值,如类别。更多关于 D3 顺序音阶的信息可以在 https://github.com/mbostock/d3/wiki/Ordinal-Scales 的文档中找到。

我们还创建了 scale 对象来将数据映射到图表的边界:

var xScale = d3.scale.ordinal()
     .rangeRoundBands([0, w], .1);
var yScale = d3.scale.linear()
     .range([h, 0]);
xScale.domain(data.map(function(d) { return d.key; }));
yScale.domain([0, d3.max(nested_data, function(d) { return d.values.length; })]);

我们接下来需要画出栅栏。我们基于分配给条的级联样式表(CSS)类创建一个选择。我们将nested_data绑定到条上,为nested_data中的每个键值创建 SVG 矩形,并将bar类分配给每个矩形;我们将很快定义类样式规则。我们将每个条形的 x 坐标设置为顺序刻度,并将 y 坐标和height属性都设置为线性刻度。

我们还添加了一个mouseover事件处理程序,并调用了一个我们很快就会创建的函数transitionVisualization()。当鼠标悬停在其中一个条形图上时,此函数会转换我们将在条形图上制作的堆叠条形图。

svg.selectAll(".bar")
     .data(nested_data)
     .enter().append("rect")
     .attr("class", "bar")
     .attr("x", function(d) { return xScale(d.key); })
     .attr("width", xScale.rangeBand())
     .attr("y", function(d) { return yScale(d.values.length) - 50; })
     .attr("height", function(d) { return h - yScale(d.values.length); })
     .on("mouseover", function(d){
          transitionVisualization (1)
     })

让我们添加一个对我们将要创建的函数drawAxes()的调用:

drawAxes()

完整的barchart()函数如下所示:

  function barchart(){
          svg.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
          var xScale = d3.scale.ordinal()
              .rangeRoundBands([0, w], .1);
          var yScale = d3.scale.linear()
              .range([h, 0]);
      xScale.domain(nested_data.map(function(d) { return d.key; }));
      yScale.domain([0, d3.max(nested_data, function(d) { return d.values.length; })]);
      svg.selectAll(".bar")
          .data(nested_data)
        .enter().append("rect")
          .attr("class", "bar")
          .attr("x", function(d) { return xScale(d.key); })
          .attr("width", xScale.rangeBand())
          .attr("y", function(d) { return yScale(d.values.length) - 50; })
          .attr("height", function(d) { return h - yScale(d.values.length); })
          .on("mouseover", function(d){
                           transitionVisualization (1)
          })
    drawAxes()
  }

让我们创建drawAxes()函数。我们把这个函数放在了d3.csv()函数的范围之外,在script标签的根处。

对于这个图表,让我们用更简单的方法,只画 x 轴。就像上一章一样,我们绘制 SVG g元素并调用xAxis对象:

function drawAxes(){
     svg.append("g")
          .attr("class", "x axis")
          .attr("transform", "translate(0," + adjustedHeight + ")")
          .call(xAxis);
}

这将绘制 x 轴,该轴为条形图提供其类别标签。

创建堆积条形图

现在我们有了一个条形图,让我们创建一个堆积条形图。首先,让我们塑造数据。我们需要一个对象数组,其中每个对象代表一个特性,并有每个级别的事件总数。

让我们从一个名为grouped_data的新数组开始:

var grouped_data = new Array();

让我们通过nested_data进行迭代,因为nested_data已经按照特性进行了分组:

nested_data.forEach(function (d) {
}

在每次遍历nested_data时,我们创建一个临时对象,并遍历values数组中的每个事件:

tempObj = {"Feature": d.key, "Sev1":0, "Sev2":0, "Sev3":0, "Sev4":0};
     d.values.forEach(function(e){
     }

values数组的每次迭代中,我们测试当前事件的严重性,并增加临时对象的适当属性:

if(e.Severity == 1)
     tempObj.Sev1++;
else if(e.Severity == 2)
     tempObj.Sev2++
else if(e.Severity == 3)
     tempObj.Sev3++;
else if(e.Severity == 4)
     tempObj.Sev4++;

创建grouped_data数组的完整代码如下所示:

nested_data.forEach(function (d) {
     tempObj = {"Feature": d.key, "Sev1":0, "Sev2":0, "Sev3":0, "Sev4":0};
     d.values.forEach(function(e){
          if(e.Severity == 1)
               tempObj.Sev1++;
          else if(e.Severity == 2)
               tempObj.Sev2++
          else if(e.Severity == 3)
               tempObj.Sev3++;
          else if(e.Severity == 4)
               tempObj.Sev4++;
     })
     grouped_data[grouped_data.length] = tempObj
});

完美!接下来,我们创建一个函数,在该函数中,我们在d3.csv()函数的范围内绘制堆积条形图:

function stackedBarChart(){
}

这就是有趣的地方。使用d3.layout.stack()函数,我们转置我们的数据,这样我们就有了一个数组,其中每个索引代表一个严重级别,并包含每个特征的一个对象,该对象具有相应严重级别的每个事件的计数:

var sevStatus = d3.layout.stack()(["Sev1", "Sev2", "Sev3", "Sev4"].map(function(sevs)
     {
          return grouped_data.map(function(d) {
          return {x: d.Feature, y: +d[sevs]};
    });
  }));

接下来,我们使用sevStatus为将要绘制的条形段的 x 和 y 值创建域图:

x.domain(sevStatus[0].map(function(d) { return d.x; }));
y.domain([0, d3.max(sevStatus[sevStatus.length - 1], function(d) { return d.y0 + d.y; })]);

接下来,我们为sevStatus数组中的每个索引绘制 SVG g元素。它们充当容器,我们在其中画出线条。我们将sevStatus绑定到这些分组元素,并设置fill属性来返回颜色数组中的一种颜色。

var sevs = svg.selectAll("g.sevs")
     .data(sevStatus)
     .enter().append("g")
     .attr("class", "sevs")
     .style("fill", function(d, i) { return z(i); });

最后,我们在刚刚创建的分组中绘制条形。我们将一个通用函数绑定到条形的data属性,该属性只传递传递给它的任何数据;这继承自 SVG 分组。

我们在不透明度设置为 0 的情况下绘制条形,因此条形最初是不可见的。我们还附加了mouseovermouseout事件处理程序,以调用transitionVisualization()——当mouseover事件被触发时传递 1,当mouseout事件被触发时传递 0(我们将很快充实transitionVisualization()的功能)。

var rect = sevs.selectAll("rect")
     .data(function(data){ return data; })
     .enter().append("svg:rect")
     .attr("x", function(d) { return x(d.x) + 13; })
     .attr("y", function(d) { return -y(d.y0) - y(d.y) + adjustedHeight; })
     .attr("class", "groupedBar")
     .attr("opacity", 0)
     .attr("height", function(d) { return y(d.y) ; })
     .attr("width", x.rangeBand() - 20)
     .on("mouseover", function(d){
          transitionVisualization (1)
     })
     .on("mouseout", function(d){
     transitionVisualization (0)
     });

完整的堆积条形图代码应该如下所示

function groupedBarChart(){
     var sevStatus = d3.layout.stack()(["Sev1", "Sev2", "Sev3", "Sev4"].map(function(sevs)
     {
          return grouped_data.map(function(d) {
          return {x: d.Feature, y: +d[sevs]};
    });
  }));
     x.domain(sevStatus[0].map(function(d) { return d.x; }));
     y.domain([0, d3.max(sevStatus[sevStatus.length - 1], function(d) { return d.y0 + d.y; })]);
  // Add a group for each sev category.
     var sevs = svg.selectAll("g.sevs")
          .data(sevStatus)
          .enter().append("g")
          .attr("class", "sevs")
          .style("fill", function(d, i) { return z(i); })
          .style("stroke", function(d, i) { return d3.rgb(z(i)).darker(); });
     var rect = sevs.selectAll("rect")
          . data(function(data){ return data; })
          .enter().append("svg:rect")
          .attr("x", function(d) { return x(d.x) + 13; })
          .attr("y", function(d) { return -y(d.y0) - y(d.y) + adjustedHeight; })
           .attr("class", "groupedBar")
          .attr("opacity", 0)
          .attr("height", function(d) { return y(d.y) ; })
          .attr("width", x.rangeBand() - 20)
          .on("mouseover", function(d){
               transitionVisualization (1)
          })
          .on("mouseout", function(d){
          transitionVisualization (0)
          });
  }

创建覆盖的可视化

但是我们还没有完成。我们一直在引用这个transitionVisualization()函数,但是我们还没有定义它。让我们现在就解决这个问题。还记得我们是如何使用它的吗:当用户将鼠标放在我们的条形图上时,我们调用transitionVisualization()并传入 1。当用户将鼠标放在我们的堆积条形图上时,我们也调用transitionVisualization()并传入一个 1。但是当用户鼠标离开堆叠条形图中的一个条时,我们调用transitionVisualization()并传入一个 0。

因此,我们传入的参数设置了堆叠条形图的不透明度。因为我们最初绘制的堆叠条形图的不透明度为 0,所以只有当用户滑过条形图中的某个条时,我们才会看到它,而当用户滚离该条时,它又会隐藏起来。

为了创造这种效果,我们使用 D3 过渡。过渡很像其他语言(如 ActionScript 3)中的补间。我们创建一个 D3 选择(在这种情况下,我们可以选择类groupedBar的所有元素),调用transition(),并设置我们想要更改的选择的属性:

function transitionVisualization(vis){
     var rect = svg.selectAll(".groupedBar")
     .transition()
     .attr("opacity", vis)
}

我们现在已经有了完整的可视化,如图 7-11 所示。

完整的代码如下所示,虽然很难通过印刷媒体演示这一功能,但您可以在 Jon 的网站上看到工作模型(可在 https://jonwestfall.com/d3/ch7.d3.example.htm 找到)或将代码放在本地 web 服务器上并自己运行:

<!DOCTYPE html>
<html>
  <head>
          <meta charset="utf-8">
    <title></title>
        <script src="d3.v3.js"></script>
        <style type="text/css">
        body {
          font: 15px sans-serif;
        }
        .axis path{
          fill: none;
          stroke: #000;
          shape-rendering: crispEdges;
        }
        .bar {
          fill: #666666;
        }
    </style>  </head>
  <body>
    <script type="text/javascript">
var w = 960,
    h = 500,
    x = d3.scale.ordinal().rangeRoundBands([0, w]),
    y = d3.scale.linear().range([0,h]),
    z = d3.scale.ordinal().range(["lightpink", "darkgray", "lightblue"])
    margin = {top: 20, right: 20, bottom: 30, left: 40},
    adjustedHeight = 500 - margin.top - margin.bottom;
        var xAxis = d3.svg.axis()
            .scale(x)
            .orient("bottom");
        var svg = d3.select("body").append("svg")
            .attr("width", w)
            .attr("height", h)
          .append("g")
        function drawAxes(){
          svg.append("g")
              .attr("class", "x axis")
              .attr("transform", "translate(0," + adjustedHeight + ")")
              .call(xAxis);
         }
         function transitionVisuaization(vis){
                 var rect = svg.selectAll(".groupedBar")
                .transition()
                .attr("opacity", vis)
         }
        d3.csv("https://jonwestfall.com/data/productionincidents.csv", function(error, data) {
            nested_data = d3.nest()
                         .key(function(d) { return d.Feature; })
                         .entries(data);
                var grouped_data = new Array();
                //for stacked bar chart
                nested_data.forEach(function (d) {
                         tempObj = {"Feature": d.key, "Sev1":0, "Sev2":0, "Sev3":0, "Sev4":0};
                         d.values.forEach(function(e){
                                 if(e.Severity == 1)
                                 tempObj.Sev1++;
                                 else if(e.Severity == 2)
                                 tempObj.Sev2++
                                 else if(e.Severity == 3)
                                 tempObj.Sev3++;
                                 else if(e.Severity == 4)
                                 tempObj.Sev4++;
                         })
                         grouped_data[grouped_data.length] = tempObj
                });
function stackedBarChart(){
  var sevStatus = d3.layout.stack()(["Sev1", "Sev2", "Sev3", "Sev4"].map(function(sevs) {
    return grouped_data.map(function(d) {
      return {x: d.Feature, y: +d[sevs]};
    });
  }));
  x.domain(sevStatus[0].map(function(d) { return d.x; }));
  y.domain([0, d3.max(sevStatus[sevStatus.length - 1], function(d) { return d.y0 + d.y; })]);
  // Add a group for each sev category.
  var sevs = svg.selectAll("g.sevs")
      .data(sevStatus)
    .enter().append("g")
      .attr("class", "sevs")
      .style("fill", function(d, i) { return z(i); });
  var rect = sevs.selectAll("rect")
      .data(function(data){ return data; })
    .enter().append("svg:rect")
      .attr("x", function(d) { return x(d.x) + 13; })
      .attr("y", function(d) { return -y(d.y0) - y(d.y) + adjustedHeight; })
          .attr("class", "groupedBar")
          .attr("opacity", 0)
      .attr("height", function(d) { return y(d.y) ; })
      .attr("width", x.rangeBand() - 20)
          .on("mouseover", function(d){
                  transitionVisuaization(1)
          })
          .on("mouseout", function(d){
                  transitionVisuaization(0)
          });
  }
  function barchart(){
          svg.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
          var xScale = d3.scale.ordinal()
              .rangeRoundBands([0, w], .1);
          var yScale = d3.scale.linear()
              .range([h, 0]);
      xScale.domain(nested_data.map(function(d) { return d.key; }));
      yScale.domain([0, d3.max(nested_data, function(d) { return d.values.length; })]);
      svg.selectAll(".bar")
          .data(nested_data)
        .enter().append("rect")
          .attr("class", "bar")
          .attr("x", function(d) { return xScale(d.key); })
          .attr("width", xScale.rangeBand())
          .attr("y", function(d) { return yScale(d.values.length) - 50; })
          .attr("height", function(d) { return h - yScale(d.values.length); })
          .on("mouseover", function(d){
                           transitionVisuaization(1)
          })
        stackedBarChart()
    drawAxes()
  }
  barchart();
});
    </script>
  </body>
</html>

摘要

本章介绍了如何使用条形图来显示生产事故环境中的分级数据。因为生产事故本质上是来自用户群的关于你的产品如何不正常或失败的直接反馈,管理生产事故是任何成熟的工程组织的关键部分。

然而,管理生产事故不仅仅是在问题出现时做出反应;它还与分析围绕您的事件的数据有关:您的应用程序的哪些领域经常中断,您在生产中看到哪些意外的使用模式可能会导致这些重复出现的问题,如何构建主动式脚手架来防止这些问题以及未来的问题。所有这些问题只有通过充分了解你的产品和数据才能回答。在这一章中,你朝着更大的理解迈出了第一步。

八、散点图相关分析

在上一章中,您了解了如何使用条形图来分析生产事故。您看到条形图非常适合显示已排序数据集中的差异,并且您使用这一想法来确定问题重复出现的区域。您还使用堆积条形图查看了生产事故严重性的细分。

本章着眼于散点图的相关性分析。散点图是在各自的轴上绘制两个独立数据集的图表,显示为笛卡尔网格(x 和 y 坐标)上的点。正如您将看到的,散点图用于尝试和识别两个数据点之间的关系。

Note

Michael Friendly 和 Daniel Denis 发表了一篇关于散点图历史的经过深思熟虑和彻底研究的论文,最初发表于 2005 年行为科学史杂志,第 41 卷,并可在 Friendly 的网站 www.datavis.ca/papers/friendly-scat.pdf 上获得。这篇文章绝对值得一读,因为它试图追溯最早记录的散点图和图表第一次被称为散点图,并且非常巧妙地描述了散点图和时间序列之间的区别(换句话说,所有的时间序列都是以时间为轴的散点图,而不是所有的散点图都是时间序列!).

在数据中查找关系

散点图上的点形成的模式或缺乏模式表明了这种关系。在很高的层面上,关系可以是

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

图 8-1

散点图显示了北美和欧洲手机总数之间的正相关关系

  • 正相关,其中一个变量随着另一个变量的增加而增加。这可以通过从左到右形成一条对角线的点来证明(见图 8-1 )。

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

图 8-2

散点图显示体重和时间流逝之间的负相关关系(对于正在节食的人)

  • 负相关,其中一个变量增加,另一个减少。这可以通过形成一条从左到右向下的线的点来证明(见图 8-2 )。

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

图 8-3

散点图显示美国历年意外死亡人数之间没有关联

  • 无相关性,由散点图显示(或不显示),散点图没有可辨别的趋势线(见图 8-3 )。

当然,简单地识别两个数据点或数据集之间的相关性并不意味着这种关系中有直接的原因——因此习惯上认为相关性并不意味着因果关系。例如,参见图 8-2 中的负相关图。如果我们假设两个轴——体重和天数——之间有直接的因果关系,我们将假设时间的流逝导致体重下降。

虽然散点图对于分析两组数据之间的关系非常有用,但是也有一种相关的模式可以用来引入第三组数据。这种可视化被称为气泡图,它使用散点图中的点的半径来展示数据的第三维。

参见图 8-4 中的气泡图,该图显示了豚鼠牙齿生长长度与服用维生素 C 剂量之间的相关性。第三个数据点是给药方式:要么补充维生素,要么喝橙汁。它作为图形中每个点的半径添加;大圆圈是维生素补充剂,小圆圈是橙汁。

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

图 8-4

豚鼠牙齿生长与维生素 C 剂量的相关性,包括维生素补充剂和橙汁

为了本章的目的,我们将使用散点图和气泡图来查看团队速度与我们关注的其他领域的隐含关系,实际上是对团队动态进行相关性分析。我们将比较团队规模和速度、速度和生产事故等等。

敏捷开发的介绍性概念

让我们从介绍敏捷开发的一些初步概念开始。如果你已经精通敏捷,这一节将是一个回顾。敏捷开发有很多种风格,但是最常见的高级概念是将大量工作时间打包的思想。时间盒使团队能够专注于一件事情并完成它,允许涉众对完成的内容快速给出反馈。这个简短的反馈循环允许团队和涉众随着需求甚至行业的变化而改变方向。

团队在工作主体上工作的这段时间——无论是一周、三周还是其他——被称为冲刺。在 sprint 结束时,团队应该有可发布的代码,尽管并不要求在每次 sprint 之后发布。

sprint 以团队定义工作主体的计划会议开始,以团队检查已完成的工作主体的回顾会议结束。在冲刺阶段,团队定期培训新的工作来完成;它定义了列出验收标准的用户故事中的工作。正是这些用户故事在每个 sprint 开始时举行的计划会议中得到优先考虑和承诺。

该流程的高级工作流程如图 8-5 所示。

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

图 8-5

敏捷开发的高级工作流

用户故事有与之相关的故事点。故事点是对故事复杂程度的估计,通常是一个数值。当团队完成冲刺时,他们开始形成一致的速度。速度是团队在一次冲刺中完成的故事点的平均数量。

速度是很重要的,因为你用它来估计你的团队在每个 sprint 开始时可以完成多少,并预测团队在一年中根据你的路线图可以完成多少积压的工作。

有很多工具可以用来管理敏捷项目,比如 Rally ( www.rallydev.com/ )或者 Atlassian 的 green hopper(www.atlassian.com/software/greenhopper/overview),也是这家公司制造了吉拉和 Confluence。无论您使用什么工具,都应该能够导出您的数据,包括每个 sprint 的用户点数。

相关分析

为了开始分析,让我们导出每个 sprint 的故事点总和以及团队名称。我们应该将所有这些数据点编译成一个文件,命名为teamvelocity.txt。我们的文件应该看起来像下面这样,它显示了名为 Red 和 Gold 的团队的 12.1 和 12.2 sprints 的数据(任意的名称,这些团队使用不同的工作主体开发相同的产品):

Sprint,TotalPoints,Team
12.1,25,Gold
12.1,63,Red
12.2,54,Red
...

让我们在那里添加一个额外的列来表示每个 sprint 的每个团队的总成员。数据现在应该是这样的:

Sprint,TotalPoints,TotalDevs,Team
12.1,25,6,Gold
12.1,63,10,Red
12.2,54,9,Red
...

我们也提供了这个样本数据集,有更多的点,这里: https://jonwestfall.com/data/teamvelocity.txt

太棒了。现在让我们将它读入 R,将第一行中的路径改为您放置它的位置:

tvFile <- "/Applications/MAMP/htdocs/teamvelocity.txt"
teamvelocity <- read.table(tvFile, sep=",", header=TRUE)

创建散点图

现在使用plot()函数创建一个散点图,比较团队在每个 sprint 中完成的总分数和每个 sprint 中团队成员的数量。我们将teamvelocity$TotalPointsteamvelocity$TotalDevs作为前两个参数传递,将类型设置为p,并为轴赋予有意义的标签:

plot(teamvelocity$TotalPoints,teamvelocity$TotalDevs, type="p", ylab="Team Members", xlab="Velocity", bg="#CCCCCC", pch=21)

这就产生了我们在图 8-6 中看到的散点图;我们可以看到,随着我们向团队中添加更多的成员,他们在迭代或 sprint 中可以完成的故事点的数量也在增加。

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

图 8-6

团队速度和团队成员总数的相关性

创建气泡图

如果我们想要更深入地了解到目前为止的数据,例如,显示哪些点属于哪个团队,我们可以用气泡图来可视化这些信息。我们可以使用symbols()函数创建气泡图。我们将TotalPointsTotalDevs传入symbols(),就像我们对plot()所做的一样,但是我们也将Team列传入一个名为circles的参数。这指定了要在图表上绘制的圆的半径。因为在我们的例子中Team是一个字符串,R 将其转换成一个因子。我们也用bg参数设置圆的颜色,用fg参数设置圆的笔画颜色。

symbols(teamvelocity$TotalPoints, teamvelocity$TotalDevs, circles=as.factor(teamvelocity$Team), inches=0.35, fg="#000000", bg="#CCCCCC", ylab="Team Members", xlab="Velocity")

前面的 R 代码应该会产生一个类似图 8-7 的气泡图。

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

图 8-7

团队速度、团队成员总数与表示团队的气泡大小的相关性

可视化 bug

图 8-7 所示的气泡图用处有限,主要是因为团队分解并不是真正相关的数据点。让我们拿起teamvelocity.txt文件,开始添加更多的信息。我们已经在第六章中讨论过追踪 bug 数据;现在让我们使用我们的 bug 跟踪软件,添加两个新的与 bug 相关的数据点:每个 sprint 结束时每个团队的 backlog 中的总 bug,以及每个 sprint 中打开了多少个 bug。我们将这些新数据点的列分别命名为BugBacklogBugsOpened

更新后的文件应该如下所示:

Sprint,TotalPoints,TotalDevs,Team,BugBacklog,BugsOpened
12.1,25,6,Gold,125,10
12.2,42,8,Gold,135,30
12.3,45,8,Gold,150,25

接下来,让我们用这些新数据创建一个散点图。我们首先将速度与每次迭代中打开的 bug 进行比较:

plot(teamvelocity$TotalPoints,teamvelocity$BugsOpened, type="p", xlab="Velocity", ylab="Bugs Opened During Sprint", bg="#CCCCCC", pch=21)

这将创建如图 8-8 所示的散点图。

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

图 8-8

团队速度和打开的 bug 的相关性

这很有趣。团队中有更多的人和完成更多的工作(或者至少完成更复杂的工作)之间存在正相关,完成的故事点越多,产生的 bug 就越多。因此,复杂性的增加与给定 sprint 中产生的 bug 数量的增加相关。至少我的数据似乎暗示了这一点。

让我们在现有的气泡图中反映这个新的数据点;我们不是按团队来划分圈子,而是按打开的 bug 来划分圈子:

symbols(teamvelocity$TotalPoints, teamvelocity$TotalDevs, circles= teamvelocity$BugsOpened, inches=0.35, fg="#000000", bg="#CCCCCC", ylab="Team Members", xlab="Velocity", main = "Velocity by Team Size by Bugs Opened")

这段代码生成如图 8-9 所示的气泡图;您可以看到气泡的大小遵循现有的正相关模式,气泡随着团队成员数量和团队速度的增加而变大。

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

图 8-9

团队速度和团队规模的相关性,其中圆圈大小表示打开的 bug

接下来,让我们创建一个散点图来查看每个 sprint 之后的总 bug backlog:

plot(teamvelocity$TotalPoints,teamvelocity$BugBacklog, type="p", xlab="Velocity", ylab="Total Bug Backlog", bg="#CCCCCC", pch=21)

该代码生成如图 8-10 所示的图表。

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

图 8-10

团队速度与总 bug 积压的相关性

这个数字表明不存在相关性。这可能是因为许多原因:也许团队一直在 sprint 期间修复 bug,或者他们正在关闭迭代过程中打开的所有 bug。确定根本原因超出了散点图的范围,但是我们可以看出,当 bug 被打开并且复杂性增加时,bug 的总积压量并没有增加。

可视化生产事件

让我们在另一个数据点进入文件的下一层;我们将针对 sprint 期间完成的工作添加一个生产事件列。具体来说,当一个 sprint 中的一部分工作完成后,它就被发布到产品中,并且一个发布版本号通常与这个发布版本相关联。我们讨论的最后一个数据点是关于在给定迭代的发布中跟踪生产中的问题。而不是迭代过程中出现的问题;迭代中完成的工作被推向生产时出现的问题。

现在让我们添加最后一列,名为ProductionIncidents:

Sprint,TotalPoints,TotalDevs,Team,BugBacklog,BugsOpened,ProductionIncidents
12.1,25,6,Gold,125,10,1
12.2,42,8,Gold,135,30,3
12.3,45,8,Gold,150,25,2

太好了!接下来,让我们用这些数据创建一个新的气泡图,比较完成的总故事点,每个迭代中打开的 bug,以及每个发布的生产事件:

symbols(teamvelocity$TotalPoints, teamvelocity$BugsOpened, circles=teamvelocity$ProductionIncidents, inches=0.35, fg="#000000", bg="#CCCCCC", ylab="Bugs Opened", xlab="Velocity", main = "Velocity by Bugs Opened by Production Incidents Opened")

该代码创建如图 8-11 所示的图表。

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

图 8-11

团队速度和打开的 bug 的相关性,其中圆圈的大小表示生产事件的数量

从这个图表中,您可以看到,至少根据我们的样本数据,对于一个给定的 sprint,完成的总故事点、打开的 bug 和打开的生产事件之间存在正相关。

最后,现在所有的数据都被分层到平面文件中,我们可以创建一个散点图矩阵。这是所有列的矩阵,用散点图相互比较。我们可以使用散点图矩阵一次性查看所有数据,并快速挑选出数据集中可能存在的任何相关模式。我们可以只用图形包中的plot()函数或pairs()函数创建散点图矩阵:

plot(teamvelocity)
pairs(teamvelocity)

任何一种都会产生如图 8-12 所示的图表。

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

图 8-12

我们完整数据集的散点图矩阵

在图 8-12 中,每行代表数据框中的一列,每个散点图代表这些列的交叉点。当您浏览矩阵中的每个散点图时,您可以清楚地看到本章已经介绍过的组合中的相关模式。虽然这是一种有效的可视化,但同时查看如此多的变量,眼睛很容易疲劳。重要的是要考虑到,仅仅因为你可以把所有的事情都放在一个数字中,你可能就不想这样做了。您可以考虑将您的数据子集化到感兴趣的特定列,使这样的图形更容易浏览。

D3 中的交互式散点图

到目前为止,在本章中,我们已经创建了不同的散点图来表示我们想要查看的数据组合。但是,如果我们想要创建一个散点图,让我们能够选择轴所基于的数据点呢?借助 D3,我们可以做到这一点!

添加基本 HTML 和 JavaScript

让我们从包含d3.js的基本 HTML 结构以及基本 CSS 开始:

<!DOCTYPE html>
<html>
  <head>
          <meta charset="utf-8">
    <title></title>
<style>
body {
  font: 15px sans-serif;
}
.axis path{
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}
.dot {
  stroke: #000;
}
</style>
</head>
<body>
<script src="d3.v3.js"></script>
</body>
</html>

接下来让我们添加script标签来保存图表。就像前面的 D3 例子一样,包括起始变量、边距、x 和 y 范围对象,以及 x 和 y 轴对象:

<script>
var margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear()
    .range([0, width]);
var y = d3.scale.linear()
    .range([height, 0]);
var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");
var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");
</script>

让我们像前面的例子一样在页面上创建 SVG 标记:

var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

加载数据

现在我们需要使用d3.csv()函数加载数据。在所有之前的 D3 例子中,大部分工作都是在回调函数的范围内完成的,但是对于这个例子,我们需要公开我们的功能,这样我们就可以通过 form select元素来改变数据点。然而,我们仍然需要从回调函数驱动初始功能,因为那时我们将拥有我们的数据,所以我们将设置我们的回调函数来调用存根公共函数。

我们为从平面文件返回的数据设置一个名为chartData的公共变量,并调用两个名为removeDots()setChartDots()的函数:

d3.csv("teamvelocity.txt", function(error, data) {
          chartData = data;
          removeDots()
          setChartDots("TotalDevs", "TotalPoints")
});

注意,我们将"TotalDevs""TotalPoints"传递给了setChartDots()函数。这是为了启动泵,因为它们将是页面加载时我们显示的初始数据点。

添加交互式功能

现在我们需要真正创造出我们已经熄灭的东西。首先,让我们在script标签的根位置创建变量chartData,在这里我们设置其他变量:

var margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom,
    chartData;

接下来,我们创建removeDots()函数,该函数选择页面上的任何圆或轴并删除它们:

function removeDots(){
      svg.selectAll("circle")
           .transition()
               .duration(0)
               .remove()
      svg.selectAll(".axis")
             .transition()
             .duration(0)
             .remove()
}

最后,我们创建了setChartDots()功能。该函数接受两个参数:xvalyval。因为我们希望确保 D3 转换已经运行完毕,并且它们有一个 250 毫秒的默认运行时间,即使我们将持续时间设置为 0,我们也将在一个setTimeout()调用中包装函数的内容,所以我们在开始绘制图表之前等待 300 毫秒。如果我们不这样做,我们可能会进入一种竞争状态,在这种状态下,当过渡从屏幕上消失时,我们正在向屏幕上绘制。

function setChartDots(xval, yval){
         setTimeout(function() {
         }, 300);
  }

在该函数中,我们使用xvalyval参数设置 x 和 y 缩放对象的域。这些参数对应于我们将绘制图表的数据点的列名:

x.domain(d3.extent(chartData, function(d) { return d[xval];}));
y.domain(d3.extent(chartData, function(d) { return d[yval];}));

接下来,我们将圆绘制到屏幕上,使用全局变量chartData来填充它,并将传入的列数据作为圆的 x 和 y 坐标。我们还在这个函数中增加了坐标轴,这样每次坐标轴改变时,我们都会重新绘制值。

svg.selectAll(".dot")
     .data(chartData)
     .enter().append("circle")
     .attr("class", "dot")
     .attr("r", 3)
     .attr("cx", function(d) { return x(d[xval]);})
     .attr("cy", function(d) { return y(d[yval]);})
     .style("fill", "#CCCCCC");
svg.append("g")
     .attr("class", "axis")
     .attr("transform", "translate(0," + height + ")")
     .call(xAxis)
 svg.append("g")
     .attr("class", "axis")
     .call(yAxis)

完整的函数应该如下所示:

function setChartDots(xval, yval){
       setTimeout(function() {
        x.domain(d3.extent(chartData, function(d) { return d[xval];}));
        y.domain(d3.extent(chartData, function(d) { return d[yval];}));
        svg.selectAll(".dot")
            .data(chartData)
          .enter().append("circle")
            .attr("class", "dot")
            .attr("r", 3)
            .attr("cx", function(d) { return x(d[xval]);})
            .attr("cy", function(d) { return y(d[yval]);})
            .style("fill", "#CCCCCC");
            svg.append("g")
                .attr("class", "axis")
                .attr("transform", "translate(0," + height + ")")
                .call(xAxis)
            svg.append("g")
                .attr("class", "axis")
                .call(yAxis)
       }, 300);
}

太棒了。

添加表单域

接下来让我们添加表单字段。我们将添加两个select元素,其中每个option对应于平面文件中的一列。这些元素调用一个 JavaScript 函数,getFormData(),我们将很快定义它:

<form>
        Y-Axis:
        <select id="yval" onChange="getFormData()">
                 <option value="TotalPoints">Total Points</option>
                 <option value="TotalDevs">Total Devs</option>
                 <option value="Team">Team</option>
                 <option value="BugsOpened">Bugs Opened</option>
                 <option value="ProductionIncidents">Production Incidents</option>
        </select>
        X-Axis:
        <select id="xval" onChange="getFormData()">
                 <option value="TotalPoints">Total Points</option>
                 <option value="TotalDevs">Total Devs</option>
                 <option value="Team">Team</option>
                 <option value="BugsOpened">Bugs Opened</option>
                 <option value="ProductionIncidents">Production Incidents</option>
        </select>
</form>

正在检索表单数据

剩下的最后一点功能是编写getFormData()函数。这个函数从两个select元素中提取选择的选项,并使用这些值传递给setChartDots()——当然是在调用removeDots()之后。

function getFormData(){
       var xEl = document.getElementById("xval")
       var yEl = document.getElementById("yval")
       var x = xEl.options[xEl.selectedIndex].value
       var y = yEl.options[yEl.selectedIndex].value
       removeDots()
       setChartDots(x,y)
}

太好了!

使用可视化

完整的源代码应该如下所示:

<!DOCTYPE html>
<html>
  <head>
          <meta charset="utf-8">
    <title></title>
<style>
body {
  font: 10px sans-serif;
}
.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}
.dot {
  stroke: #000;
}
</style>
</head>
<body>
        <form>
                Y-Axis:
                <select id="yval" onChange="getFormData()">
                         <option value="TotalPoints">Total Points</option>
                         <option value="TotalDevs">Total Devs</option>
                         <option value="Team">Team</option>
                         <option value="BugsOpened">Bugs Opened</option>
                         <option value="ProductionIncidents">Production Incidents</option>
                </select>
                X-Axis:
                <select id="xval" onChange="getFormData()">
                         <option value="TotalPoints">Total Points</option>
                         <option value="TotalDevs">Total Devs</option>
                         <option value="Team">Team</option>
                         <option value="BugsOpened">Bugs Opened</option>
                         <option value="ProductionIncidents">Production Incidents</option>
                </select>
        </form>
<script src="d3.v3.js"></script>
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 40},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom,
        chartData;
var x = d3.scale.linear()
    .range([0, width]);
var y = d3.scale.linear()
    .range([height, 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", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
    svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis)
    svg.append("g")
        .attr("class", "y axis")
        .call(yAxis)
   function getFormData(){
          var xEl = document.getElementById("xval")
          var yEl = document.getElementById("yval")
          var x = xEl.options[xEl.selectedIndex].value
          var y = yEl.options[yEl.selectedIndex].value
          removeDots()
          setChartDots(x,y)
   }
   function removeDots(){
         svg.selectAll("circle")
              .transition()
                  .duration(0)
                  .remove()
         svg.selectAll(".axis")
                 .transition()
                .duration(0)
                .remove()
   }
  function setChartDots(xval, yval){
         setTimeout(function() {
          x.domain(d3.extent(chartData, function(d) { return d[xval];}));
          y.domain(d3.extent(chartData, function(d) { return d[yval];}));
          svg.selectAll(".dot")
              .data(chartData)
            .enter().append("circle")
              .attr("class", "dot")
              .attr("r", 3)
              .attr("cx", function(d) { return x(d[xval]);})
              .attr("cy", function(d) { return y(d[yval]);})
              .style("fill", "#CCCCCC");
               svg.append("g")
                  .attr("class", "axis")
                  .attr("transform", "translate(0," + height + ")")
                  .call(xAxis)
              svg.append("g")
                  .attr("class", "axis")
                  .call(yAxis)
         }, 300);
  }
d3.csv("teamvelocity.txt", function(error, data) {
          chartData = data;
          removeDots()
          setChartDots("TotalDevs", "TotalPoints")
});
</script>
</body>
</html>

它应该创建如图 8-13 所示的交互式可视化。

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

图 8-13

使用 D3 的交互式散点图

摘要

这一章着眼于团队行动的速度与漏洞和生产问题的出现之间的相互关系。这些数据点之间自然存在正相关关系:当我们制造新的东西时,我们为那些新的东西和现有的东西创造了新的机会去打破。

当然,这并不意味着我们应该停止制造新的东西,即使出于某种原因,我们的业务部门和我们的行业允许这样做。这意味着我们需要在创造新事物和培育及维护现有事物之间找到平衡。这正是我们将在下一章看到的。

九、使用平行坐标可视化交付和质量的平衡

最后一章着眼于使用散点图来确定数据集之间的关系。它讨论了数据集之间可能存在的不同类型的关系,例如正相关和负相关。我们在团队动力学的前提下表达了这个想法:你看到团队中的人数和团队能够完成的工作量之间,或者完成的工作量和产生的缺陷数量之间有任何关联吗?

在这一章中,我们将一直在谈论的关键概念联系在一起:可视化、团队特征工作、缺陷和生产事件。我们将使用名为平行坐标的数据可视化将它们联系在一起,以显示这些工作之间的平衡。

什么是平行坐标图?

平行坐标图是由 N 个垂直轴组成的可视化图,每个轴代表一个唯一的数据集,并在轴上绘制线条。线条显示了轴之间的关系,很像散点图,线条形成的图案表明了这种关系。当我们看到一簇线时,我们还可以收集关于轴之间关系的细节。让我们以图 9-1 中的图表为例来看看这一点。

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

图 9-1

安全带数据集的平行坐标

我从 r 内置的数据集Seatbelts构建了图 9-1 中的图表。安全带在 R 命令行。我提取了可用列的子集,以便更好地突出显示数据中的关系:

cardeaths <- data.frame(Seatbelts[,1], Seatbelts[,5], Seatbelts[,6], Seatbelts[,8])
colnames(cardeaths) <- c("DriversKilled", "DistanceDriven", "PriceofGas", "SeatbeltLaw")

该数据集代表了强制系安全带前后英国死于车祸的司机人数。坐标轴代表死亡司机的数量,行驶的距离,当时的汽油成本,以及是否有安全带法。

查看平行坐标有许多有用的方法。如果我们观察一对轴之间的连线,我们可以看到这些数据集之间的关系。例如,如果我们观察汽油价格和安全带法律之间的关系,我们可以看到,当安全带法律存在时,汽油价格受到非常严格的约束,但当安全带法律不存在时,汽油价格涵盖了很大范围的价格(即,许多不同的线汇聚在代表法律之前的时间的点上,一条窄带线汇聚在法律通过后的时间上)。这种关系可能意味着许多不同的事情,但因为我知道这些数据,我知道这是因为在法律实施后,我们的死亡样本量要小得多:在安全带法律实施前有 14 年的数据,但在安全带法律实施后只有 2 年的数据。

我们还可以沿着所有轴追踪线条,看看每个轴是如何关联的。对于所有颜色相同的线条,这是很难做到的,但是当我们改变线条的颜色和阴影时,我们可以更容易地看到图表上的图案。让我们拿现有的图表,给线条分配颜色(结果如图 9-2 所示;此外,如果您还没有软件包,您需要安装它):

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

图 9-2

安全带数据集的平行坐标,每条线有不同的灰色阴影

library(MASS)
parcoord(cardeaths, col=rainbow(length(cardeaths[,1])), var.label=TRUE)

Note

您需要导入批量库来使用parcoord()功能。

图 9-2 开始显示数据中存在的模式。死亡人数最少的线路行驶的距离也最长,并且主要落在安全带法颁布后的时间点。同样,请注意,安全带后法律的样本量确实比安全带前法律的样本量小得多,但您可以看到,能够追踪这些数据点的相互联系是多么有用和有意义。

平行坐标图的历史

在纵轴上使用平行坐标的想法是 1885 年由 Maurice d’Ocagne 发明的,当时他创建了诺模图和诺模图领域。诺模图是根据数学规则计算数值的工具。至今仍在使用的诺谟图的经典例子是温度计上同时显示华氏温度和摄氏温度的线条。或者想想尺子,一边以英寸显示数值,另一边以厘米显示数值。

Note

罗恩·多弗勒写了一篇关于诺模图的长篇论文,请点击这里: http://myreckonings.com/wordpress/2008/01/09/the-art-of-nomography-i-geometric-design/ 。Doerfler 还主持了一个名为 modern nomograms(www.myreckonings.com/modernnomograms/))的网站,该网站“提供专为当今应用而设计的引人注目且有用的图形计算器”

你可以在图 9-3 和 9-4 中看到现代诺谟图的例子,由罗恩·多弗勒提供。

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

图 9-4

罗恩·多弗勒、叶小开·罗舍尔和乔·马拉斯科提供的曲线比例尺诺谟图

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

图 9-3

展示函数 S、P、R 和 T 之间数值转换的诺模图,这是顺序概率比测试的基础

Note

术语平行坐标及其代表的概念是阿尔弗雷德·因塞尔伯格在伊利诺伊大学学习期间推广并重新发现的。Inselberg 博士目前是特拉维夫大学的教授和圣地亚哥超级计算中心的高级研究员。Inselberg 博士还出版了一本关于这个主题的书,平行坐标:视觉多维几何及其应用 (Springer,2009)。他还发表了一篇关于如何有效阅读和使用平行坐标的论文,题目是“多维侦探”,可从 IEEE 获得。

寻找平衡

我们知道平行坐标用于可视化多个变量之间的关系,但这如何应用于我们在本书中迄今为止一直在谈论的内容呢?到目前为止,我们讨论了量化和可视化缺陷积压、生产事件的来源,甚至我们团队承诺的工作量。可以说,平衡产品开发的这些方面可能是团队所做的最具挑战性的活动之一。

在每一次正式或非正式的迭代中,团队成员都必须决定他们应该在每一个关注点上投入多少精力:开发新功能、修复现有功能的缺陷,以及根据用户的直接反馈解决生产事件。这些只是每个产品团队必须处理的细微差别的一个例子;他们可能还必须考虑花费在技术债务或更新基础设施上的时间。

我们可以使用平行坐标来形象化这种平衡,既可以作为文档,也可以作为开始新的 sprints 时的分析工具。

创建平行坐标图

创建平行坐标图有几种不同的方法。使用前一章的数据,我们可以查看每次迭代的运行总数。回想一下,数据是每个迭代中提交的点的总数,以及每个团队的 backlog 中有多少 bug 和产品事件,在迭代中有多少新 bug,以及团队中有多少成员。数据看起来很像这样:

  Sprint TotalPoints TotalDevs Team   BugBacklog BugsOpened ProductionIncidents
  1      12.10       25        6 Gold 125        10         1
  2      12.20       42        8 Gold 135        30         3
  3      12.30       45        8 Gold 150        25         2
  4      12.40       43        8 Gold 149        23         3
  5      12.50       32        6 Gold 155        24         1
  6      12.60       43        8 Gold 140        22         4
  7      12.70       35        7 Gold 132         9         1
...

为了利用这些数据,我们可以把它读入 R,就像我们在上一章所做的那样:

tvFile <- "/Applications/MAMP/htdocs/teamvelocity.txt"
teamvelocity <- read.table(tvFile, sep=",", header=TRUE)

然后,我们可以创建一个新的数据框,其中包含来自teamvelocity变量的所有列,除了Team列。该列是一个字符串,如果我们在传递给它的对象中包含字符串,我们在本例中使用的 R parcoord()函数就会抛出一个错误。团队信息在这种情况下也没有意义。图表中的线条将代表我们的团队:

t<- data.frame(teamvelocity$Sprint, teamvelocity$TotalPoints, teamvelocity$TotalDevs, teamvelocity$BugBacklog, teamvelocity$BugsOpened, teamvelocity$ProductionIncidents)
colnames(t) <- c("sprint", "points", "devs", "total bugs", "new bugs", "prod incidents")

我们将新对象传递给parcoord()函数。我们还将rainbow()函数传递给color参数,并将var.label参数设置为true,以使每个轴的上下边界在图表上可见:

parcoord(t, col=rainbow(length(t[,1])), var.label=TRUE)

这段代码产生了如图 9-5 所示的可视化效果。

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

图 9-5

整个组织度量的不同方面的平行坐标图,包括每个迭代的承诺点、团队的总开发人员、总 bug backlog、新 bugs open 和生产事件

图 9-5 为我们呈现了一些有趣的故事。我们可以看到,在我们的数据集中,一些团队在承担更多分价值的工作时会产生更多的错误。其他团队有大量的 bug backlog,但在每次迭代中没有创建大量的新 bug,这意味着他们没有关闭他们打开的 bug。有些团队比其他团队更稳定。所有这些都包含团队可以用来反思和持续改进的见解。但最终这张图表是反应性的,围绕主要问题展开讨论。它告诉我们每个 sprint 对我们各自的积压工作的影响,包括 bug 和生产事件。它还告诉我们在每个 sprint 期间打开了多少个 bug。

图中没有显示的是针对每个积压工作所花费的工作量。为了说明这一点,我们需要做一些准备工作。

增加努力

在过去的章节中,我提到了 Greenhopper 和 Rally,它们是计划迭代、对积压工作进行优先级排序以及跟踪用户故事进展的方法。无论你选择哪种产品,它都应该提供某种方式来用元数据对你的用户故事进行分类或标记。不需要您的软件支持就可以完成这种分类的一些非常简单的方法包括:

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

图 9-6

按类别、缺陷、特性或产品事件标记的用户故事(由 Rally 提供)

  • 在每个用户故事的标题中添加标签(参见图 9-6 中的例子,看看在 Rally 中会是什么样子)。使用这种方法,您需要手动地或者以编程的方式合计每个类别的工作量。

  • 为每个工作描述嵌套子项目。

无论您如何着手创建这些存储桶,您都应该有一种方法来跟踪您的类别在每个 sprint 期间所花费的工作量。为了直观显示这一点,只需将其从您最喜欢的工具中导出到一个平面文件中,类似于下面所示的结构:

iteration,defect,prodincidents,features,techdebt,innovation
13.1,6,3,13,2,1
13.2,8,1,7,2,1
13.3,10,1,9,3,2
13.5,9,2,18,10,3
13.6,7,5,19,8,3
13.7,9,5,21,12,3
13.8,6,7,11,14,3
13.9,8,3,16,18,3
13.10,7,4,15,5,3

为了开始使用这些数据,我们需要将平面文件的内容导入到 r 中。我们将数据存储在一个名为teamEffort and的变量中,并将teamEffort传递给parcoord()函数:

teFile <- "/Applications/MAMP/htdocs/teamEffort.txt"
teamEffort <- read.table(teFile, sep=",", header=TRUE)
parcoord(teamEffort, col=rainbow(length(teamEffort[,1])), var.label=TRUE, main="Level of Effort Spent")

该代码生成如图 9-7 所示的图表。

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

图 9-7

每个计划所花费的努力水平的平行坐标图

这个图表不是关于数据隐含的关系,而是关于每个 sprint 的明确的努力程度。在真空中,这些数据点是没有意义的,但是当您查看这两个图表并比较总的 bug 积压和总的生产事件时,与解决其中任何一个所花费的努力水平相比,您开始看到团队需要解决的盲点。盲点可能是有大量 bug 积压或生产事件计数的团队没有花费足够的精力来解决这些积压。

用 D3 刷平行坐标图

阅读密集平行坐标图的诀窍是使用一种称为刷的技术。笔刷会淡化图表上所有线条的颜色或不透明度,除了您想要沿坐标轴描摹的线条。我们可以使用 D3 实现这种程度的交互性。

创建基础结构

让我们首先使用我们的基本 HTML 框架结构创建一个新的 HTML 文件:

<!DOCTYPE html>
<html>
  <head>
          <meta charset="utf-8">
    <title></title>
</head>
<body>
<script src="d3.v3.js"></script>
</body>
</html>

然后我们创建一个新的script标签来保存图表的 JavaScript。在这个标记中,我们首先创建设置图表的高度和宽度所需的变量、保存边距值的对象、轴列名的数组以及 x 对象的 scale 对象。

我们还创建了引用 D3 SVG line 对象的变量,一个对 D3 轴的引用,以及一个名为foreground的变量来保存所有路径的分组,这些路径将是图表中轴之间绘制的线:

<script>
var margin = {top: 80, right: 160, bottom: 200, left: 160},
     width = 1280 - margin.left - margin.right,
     height = 800 - margin.top - margin.bottom,
         cols = ["iteration","defect","prodincidents","features","techdebt","innovation"]
var x = d3.scale.ordinal().domain(cols).rangePoints([0, width]),
    y = {};
var line = d3.svg.line(),
    axis = d3.svg.axis().orient("left"),
    foreground;
</script>

我们将 SVG 元素绘制到页面上,并将其存储在一个名为 svg 的变量中:

var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
We use d3.csv to load in the teameffort.txt flat file:
d3.csv("teameffort.txt", function(error, data) {
}

到目前为止,我们遵循与前几章相同的格式:在顶部布置变量,创建 SVG 元素,并加载数据;大多数数据相关的逻辑发生在匿名函数中,该函数在数据加载后触发。

对于平行坐标,这个过程在这里稍有改变,因为我们需要为数据中的每一列创建 y 轴。

为每列创建一个 Y 轴

要为每一列创建 y 轴,我们必须遍历保存列名的数组,将每一列的内容显式转换为数字,在y变量中为每一列创建一个索引,并为每一列创建一个 D3 scale对象:

cols.forEach(function(d) {
        //convert to numbers
        data.forEach(function(p) { p[d] = +p[d]; });
        //create y scale for each column
        y[d] = d3.scale.linear()
                .domain(d3.extent(data, function(p) { return p[d]; }))
               .range([height, 0]);
});

划清界限

我们需要画出穿过每个轴的线,所以我们创建一个 SVG 分组来聚集和保存所有的线。我们将foreground类分配给分组(这样做很重要,因为我们将通过 CSS 处理刷行):

foreground = svg.append("g")
        .attr("class", "foreground")

我们将 SVG 路径附加到这个分组中。我们将数据附加到路径上,将路径的颜色设置为随机生成的颜色,并找出mouseovermouseout事件处理程序。我们还将路径的d属性设置为我们将要创建的函数path()

我们一会儿将回到这些事件处理程序。

foreground = svg.append("g")
    .attr("class", "foreground")
  .selectAll("path")
    .data(data)
  .enter().append("path")
.attr("stroke", function(){return "#" + Math.floor(Math.random()*16777215).toString(16);})
    .attr("d", path)
    .attr("width", 16)
        .on("mouseover", function(d){
        })
        .on("mouseout", function(d){
        })

让我们来充实一下path()函数。在这个函数中,我们接受一个名为d的参数,它将是data变量的索引。该函数返回带有 x 和 y 刻度的路径坐标映射。

function path(d) {
     return line(cols.map(function(p) { return x(p), y[p]; }));
}

path()函数返回如下所示的数据——一个多维数组,每个索引和数组由两个坐标值组成:

[[0, 520], [192, 297.14285714285717], [384, 346.6666666666667], [576, 312], [768, 491.1111111111111], [960, 520]]

淡化线条

让我们退一步想想。为了处理笔刷,我们需要创建一个样式规则来淡化线条的不透明度。所以让我们回到页面的head部分,创建一个style标签和一些样式规则。

我们将path.fade设置为选择器,并将笔画不透明度设置为 4%。同时,我们还设置了正文字体样式和路径样式。

<style>
body {
  font: 15px sans-serif;
  font-weight:normal;
}
path{
  fill: none;
  shape-rendering: geometricPrecision;
  stroke-width:1;
}
path.fade {
  stroke: #000;
  stroke-opacity: .04;
}
</style>

让我们回到 stubbed out 事件处理程序。D3 提供了一个名为classed()的函数,允许我们将类添加到选择中。在mouseover处理程序中,我们使用classed()函数将刚刚创建的fade样式应用于前景中的每条路径。它会淡出每一行。接下来,我们将针对当前选择,使用d3.select(this)classed()来关闭淡入淡出样式。

mouseout处理程序中,我们关闭了fade样式:

foreground = svg.append("g")
     .attr("class", "foreground")
   .selectAll("path")
     .data(data)
   .enter().append("path")
 .attr("stroke", function(){return "#" + Math.floor(Math.random()*16777215).toString(16);})
     .attr("d", path)
     .attr("width", 16)
         .on("mouseover", function(d){
                 foreground.classed("fade",true)
                 d3.select(this).classed("fade", false)
        })
        .on("mouseout", function(d){
                 foreground.classed("fade",false)
        })

创建轴

最后,我们需要创建轴:

var g = svg.selectAll(".column")
               .data(cols)
             .enter().append("svg:g")
               .attr("class", "column")
                   .attr("stroke", "#000000")
               .attr("transform", function(d) { return "translate(" + x(d) + ")"; })
           // Add an axis and title.
           g.append("g")
               .attr("class", "axis")
               .each(function(d) { d3.select(this).call(axis.scale(y[d])); })
             .append("svg:text")
               .attr("text-anchor", "middle")
               .attr("y", -19)
               .text(String);

我们的完整代码如下:

<!DOCTYPE html>
<html>
  <head>
          <meta charset="utf-8">
    <title></title>
<style>
body {
  font: 15px sans-serif;
  font-weight:normal;
}
path{
  fill: none;
  shape-rendering: geometricPrecision;
  stroke-width:1;
}
path.fade {
  stroke: #000;
  stroke-opacity: .04;
}
</style>
</head>
<body>
<script src="d3.v3.js"></script>
<script>
var margin = {top: 80, right: 160, bottom: 200, left: 160},
     width = 1280 - margin.left - margin.right,
     height = 800 - margin.top - margin.bottom,
         cols = ["iteration","defect","prodincidents","features","techdebt","innovation"]
var x = d3.scale.ordinal().domain(cols).rangePoints([0, width]),
    y = {};
var line = d3.svg.line(),
    axis = d3.svg.axis().orient("left"),
    foreground;
var svg = d3.select("body").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("teameffort.txt", function(error, data) {
        cols.forEach(function(d) {
                //convert to numbers
            data.forEach(function(p) { p[d] = +p[d]; });
            y[d] = d3.scale.linear()
                .domain(d3.extent(data, function(p) { return p[d]; }))
                .range([height, 0]);
                   });
           foreground = svg.append("g")
               .attr("class", "foreground")
             .selectAll("path")
               .data(data)
             .enter().append("path")
           .attr("stroke", function(){return "#" + Math.floor(Math.random()*16777215).toString(16);})
               .attr("d", path)
               .attr("width", 16)
                  .on("mouseover", function(d){
                          foreground.classed("fade",true)
                          d3.select(this).classed("fade", false)
                  })
                  .on("mouseout", function(d){
                           foreground.classed("fade",false)
                  })
           var g = svg.selectAll(".column")
               .data(cols)
             .enter().append("svg:g")
               .attr("class", "column")
                   .attr("stroke", "#000000")
               .attr("transform", function(d) { return "translate(" + x(d) + ")"; })
           // Add an axis and title.
           g.append("g")
               .attr("class", "axis")
               .each(function(d) { d3.select(this).call(axis.scale(y[d])); })
             .append("svg:text")
               .attr("text-anchor", "middle")
               .attr("y", -19)
               .text(String);
                function path(d) {
                     return line(cols.map(function(p) { return x(p), y[p]; }));
                 }
          });
</script>
</body>
</html>

该代码生成如图 9-8 所示的图表。

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

图 9-8

在 D3 中创建的平行坐标图

如果我们将鼠标滑过任何线条,我们会看到如图 9-9 所示的笔刷效果,其中除了鼠标当前滑过的线条,所有线条的不透明度都会缩小。

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

图 9-9

交互式刷图平行坐标图

摘要

本章介绍了平行坐标图。你可以领略一下它们的历史——它们最初是如何以列线图的形式出现的,用来显示价值转换。您在可视化团队如何在迭代过程中平衡产品开发的不同方面的上下文中查看了它们的实际应用。

平行坐标是本书涵盖的最后一种可视化类型,但它远不是最后一种可视化类型。这本书远不是这个问题的最终结论。每学期结束时,我都会告诉我的学生,我希望他们能继续使用他们在我的课上学到的东西。只有通过使用所涵盖的语言或主题,通过不断地使用它,探索它,并测试它的边界,学生才会将这一新工具融入他们自己的技能组合中。否则,如果他们离开课堂(或者,在这种情况下,合上书)并且很长一段时间不思考这个主题,他们可能会忘记我们所学的大部分内容。

如果你是一名开发人员或技术领导者,我希望你读了这本书,并受到启发,开始跟踪自己的数据。这只是你可以追踪的一小部分东西。正如我的书Pro JavaScript Performance:Monitoring and Visualization中所述,您可以对代码进行检测以跟踪性能指标,或者您可以使用 Splunk 等工具来创建仪表板,以可视化使用数据和错误率。您可以直接进入源代码存储库数据库,获得诸如一周中哪些时间和哪些天有最多的提交活动之类的指标,以避免在这些时间安排会议。

所有这些数据跟踪的要点是自我完善——建立你目前所处位置的基线,并跟踪你想要达到的目标,不断完善你的技能,并在你所做的事情上表现出色。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值