javascript可视化
在本文中,我想带您看一下我最近构建的一个示例项目-使用D3库的完全原始的可视化类型,它展示了如何将这些组件加在一起以使D3成为一个很好的学习库。
D3代表数据驱动文档。 这是一个JavaScript库,可用于制作各种出色的数据可视化效果和图表。
如果您曾经看过《纽约时报》上任何精彩的互动故事 ,那么您将已经看到了D3的实际应用。 你也可以看到,已建成与D3大项目的一些很酷的例子在这里 。
对于D3来说,学习曲线非常陡峭,因为D3有一些您可能从未见过的特殊怪癖 。 但是,如果您可以通过学习足够的D3危险的第一阶段,那么您很快就能为自己构建一些非常酷的东西。
真正使D3脱颖而出的三个主要因素是:
- 灵活性强 。 D3允许您获取任何类型的数据,并将其与浏览器窗口中的形状直接关联。 这些数据绝对可以是任何东西 ,从而允许使用大量有趣的用例来创建完全原始的可视化效果。
- 高雅 。 添加更新之间的平滑过渡的交互式元素很容易。 该库的编写精美 ,一旦掌握了语法,就很容易保持代码整洁。
- 社区 。 已经有一个由D3组成的出色开发人员的庞大生态系统,他们可以随时在线共享其代码。 您可以使用诸如网站bl.ocks.org和blockbuilder.orgSwift被别人找到预先编写的代码,这些代码片段直接复制到自己的项目。
该项目
作为大学的经济学专业,我一直对收入不平等感兴趣。 我参加了几节关于该主题的课程,这让我感到震惊,因为它在应有的程度上还没有被完全理解。
我开始使用Google的Public Data Explorer探索收入不平等问题……
如果对通货膨胀进行调整,尽管每位工人的生产率一直在飞速增长,但对于最底层的40%的家庭来说,家庭收入几乎保持不变 。 实际上,只有前20%的用户获得了更多的收益(在这个范围内,如果您查看前5%的用户,则差异甚至会更令人震惊)。
这是一条我想以令人信服的方式传达的信息,它提供了使用D3.js的绝佳机会,因此我开始草拟一些想法。
素描
因为我们正在使用D3,所以我大概或多或少地开始草拟出我能想到的任何东西 。 制作简单的折线图,条形图或气泡图很容易,但是我想做些不同的事情。
我发现人们倾向于使用最常见的类比来反驳关于不平等的担忧,即“如果饼变得更大 ,那么还有更多的余地”。 直觉是,如果GDP的总份额设法大幅度增加,那么即使有些人变得越来越瘦 ,他们的境况也会更好 。 但是,正如我们所看到的,馅饼有可能变得更大,而人们总体上却变得更少。
我的可视化此数据的第一个想法是这样的:
想法是,我们将拥有这个脉动的饼图,每个切片代表美国收入分配的五分之一。 每个饼图的面积将与该部分人口获得多少收入相关,图表的总面积将代表其总GDP。
但是,我很快遇到了一个问题。 事实证明,人脑在区分不同区域的大小方面异常出色 。 当我更具体地将其映射出来时,该消息并不像应有的那么明显:
在这里,实际上,最贫穷的美国人似乎随着时间的推移变得越来越富有 ,这证实了直觉上似乎是正确的。 我再考虑了这个问题,我的解决方案包括保持每个圆弧的角度恒定,并使每个圆弧的半径动态变化。
免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
这实际上是如何实现的:
我想指出的是,此图像仍然倾向于低估此处的效果。 如果使用简单的条形图,效果将更加明显:
但是,我致力于做出独特的可视化效果,我想向大家传达这样的信息: 馅饼可以变大 ,而一部分可以变小 。 现在我有了主意,是时候用D3来构建它了。
借入代码
因此,既然我知道我要构建什么,现在该开始深入了解该项目,并开始编写一些代码 。
您可能认为我会从头开始编写我的前几行代码,但是您会错了。 这是D3,并且由于我们正在使用D3,所以我们总是可以从社区中找到一些预先编写的代码来开始我们的工作。
我们正在创建一个全新的东西,但是它与常规饼图有很多共同之处,因此我快速浏览了bl.ocks.org ,我决定采用Mike Bostock的经典实现 , D3的创造者。 该文件可能已经被复制了数千次,并且编写该文件的人是使用JavaScript的真正向导,因此我们可以确定我们已经从一个不错的代码块开始。
该文件是用D3 V3编写的,由于第5版最终于上个月发布,因此现在已过期两个版本。 D3 V4的一大变化是该库切换为使用平面命名空间,因此像d3.scale.ordinal()
类的scale函数的编写方式类似于d3.scaleOrdinal()
。 在版本5中,最大的变化是数据加载功能现在被构造为Promises ,这使得一次处理多个数据集变得更加容易。
为避免混淆,我已经经历了创建此代码的更新V5版本的麻烦,并将其保存在blockbuilder.org上 。 我还转换了语法以使其符合ES6约定,例如将ES5匿名函数切换为箭头函数。
这是我们已经开始的内容:
然后,我将这些文件复制到我的工作目录中,并确保可以在自己的计算机上复制所有内容。 如果您想自己按照本教程进行操作,则可以从我们的GitHub存储库中克隆此项目 。 您可以从文件starter.html
的代码开始。 请注意,您将需要一台服务器(例如this )来运行此代码,因为它实际上依赖于Fetch API来检索数据。
让我简要介绍一下此代码的工作方式。
遍历我们的代码
首先,我们在文件顶部声明一些常量,这些常量将用于定义饼图的大小:
const width = 540;
const height = 540;
const radius = Math.min(width, height) / 2;
这使我们的代码具有超级可重用性,因为如果我们想使其更大或更小,则只需要担心在此处更改这些值即可。
接下来,我们将SVG画布添加到屏幕上。 如果您对SVG不太了解,则可以将画布视为页面上可以绘制形状的空间。 如果我们尝试在此区域之外绘制SVG,则它根本不会显示在屏幕上:
const svg = d3.select("#chart-area")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${width / 2}, ${height / 2})`);
我们通过调用d3.select()
来抓住一个空的div,其ID为chart-area
。 我们还使用d3.append()
方法附加了SVG画布,并使用d3.attr()
方法为其宽度和高度设置了一些尺寸。
我们还在此画布上附加了SVG组元素,这是一种特殊类型的元素,可用于将元素构造在一起。 这使我们能够使用组元素的transform
属性将整个可视化效果移到屏幕的中心。
之后,我们将设置一个默认的比例尺,该比例尺将用于为馅饼的每个切片分配新的颜色:
const color = d3.scaleOrdinal(["#66c2a5", "#fc8d62", "#8da0cb","#e78ac3", "#a6d854", "#ffd92f"]);
接下来,我们用几行代码来设置D3的饼图布局:
const pie = d3.pie()
.value(d => d.count)
.sort(null);
在D3中, 布局是特殊功能,我们可以调用一组数据。 布局函数接收特定格式的数据数组,并吐出带有一些自动生成的值的转换后的数组 ,然后我们可以对其进行处理。
然后,我们需要定义一个路径生成器 ,可以用来绘制弧线。 路径生成器使我们能够在Web浏览器中绘制路径SVG。 D3真正要做的只是将数据与屏幕上的形状相关联,但是在这种情况下,我们想定义一个比简单的圆形或正方形更复杂的形状。 路径SVG通过为要在其间绘制的线定义路线来工作,我们可以使用其d
属性来定义。
这可能是这样的:
<svg width="190" height="160">
<path d="M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80" stroke="black" fill="transparent"/>
</svg>
d
属性包含一种特殊的编码,该编码使浏览器可以绘制所需的路径。 如果您真的想知道此字符串的含义,可以在MDN的SVG文档中找到有关它的信息 。 对于用D3进行编程,我们实际上不需要了解任何有关这种特殊编码的信息,因为我们有生成器可以为我们吐出d
属性,我们只需要使用一些简单的参数进行初始化即可。
对于圆弧,我们需要为路径生成器提供一个innerRadius
和一个outerRadius
值(以像素为单位),并且生成器将整理用于计算每个角度的复杂数学:
const arc = d3.arc()
.innerRadius(0)
.outerRadius(radius);
对于我们的图表,我们为innerRadius
使用的值为零,这为我们提供了一个标准的饼图。 但是,如果我们想绘制一个甜甜圈图 ,那么我们所要做的就是插入一个小于我们的outerRadius
值的值。
经过几个函数声明后,我们使用d3.json()
函数加载数据:
d3.json("data.json", type).then(data => {
// Do something with our data
});
在D3版本5.x中,对d3.json()
的调用返回Promise ,这意味着D3将获取在给定的相对路径中找到的JSON文件的内容,并执行我们要执行的功能加载完后, then()
调用then()
方法。然后,我们可以在回调的data
参数中访问正在查看的对象。
我们还在这里传递了一个函数引用- type
-它将把我们要加载的所有值都转换成数字,以后可以使用:
function type(d) {
d.apples = Number(d.apples);
d.oranges = Number(d.oranges);
return d;
}
如果我们添加一个console.log(data);
在d3.json
回调的顶部的语句中,我们可以看一下现在正在使用的数据:
{apples: Array(5), oranges: Array(5)}
apples: Array(5)
0: {region: "North", count: "53245"}
1: {region: "South", count: "28479"}
2: {region: "East", count: "19697"}
3: {region: "West", count: "24037"}
4: {region: "Central", count: "40245"}
oranges: Array(5)
0: {region: "North", count: "200"}
1: {region: "South", count: "200"}
2: {region: "East", count: "200"}
3: {region: "West", count: "200"}
4: {region: "Central", count: "200"}
我们的数据在这里分为两个不同的数组,分别代表苹果和橙子的数据。
在这一行中,每当单击一个单选按钮时,我们将切换正在查看的数据:
d3.selectAll("input")
.on("change", update);
我们还需要在可视化的第一次运行中调用update()
函数,并传入一个初始值(带有“ apples”数组)。
update("apples");
让我们看一下我们的update()
函数在做什么。 如果您是D3的新手,这可能会引起一些困惑,因为它是D3最难理解的部分之一...
function update(value = this.value) {
// Join new data
const path = svg.selectAll("path")
.data(pie(data[value]));
// Update existing arcs
path.transition().duration(200).attrTween("d", arcTween);
// Enter new arcs
path.enter().append("path")
.attr("fill", (d, i) => color(i))
.attr("d", arc)
.attr("stroke", "white")
.attr("stroke-width", "6px")
.each(function(d) { this._current = d; });
}
首先,我们为value
使用默认的函数参数。 如果我们将参数传递给update()
函数(当我们第一次运行它时),我们将使用该字符串,否则我们将从以下事件的click
事件中获得所需的值:我们的无线电输入。
然后,我们在D3中使用常规更新模式来处理弧的行为。 这通常涉及执行数据联接,退出旧元素,更新屏幕上的现有元素以及添加添加到我们的数据中的新元素。 在此示例中,我们不必担心退出元素,因为我们在屏幕上始终具有相同数量的饼图切片。
首先,有我们的数据联接:
// JOIN
const path = svg.selectAll("path")
.data(pie(data[val]));
每次我们的可视化更新时,都会将新的数据数组与屏幕上的SVG相关联。 我们正在将数据(“苹果”数组或“橙色”数组)传递到pie()
布局函数中,该函数计算一些开始和结束角度,这些角度可用于绘制圆弧。 现在,此path
变量包含屏幕上所有弧的特殊虚拟选择 。
接下来,我们将更新屏幕上仍存在于数据数组中的所有SVG。 我们在这里添加了一个过渡 -D3库的一项奇妙功能-将这些更新传播200毫秒以上:
// UPDATE
path.transition().duration(200)
.attrTween("d", arcTween);
我们在d3.transition()
调用上使用attrTween()
方法来定义一个自定义过渡,D3应该使用该自定义过渡来更新其每个圆弧的位置(使用d
属性进行过渡)。 如果我们尝试向大多数属性添加过渡,则无需执行此操作,但是我们需要执行此操作以在不同路径之间进行过渡。 D3不能真正弄清楚如何在自定义路径之间进行转换,因此我们使用arcTween()
函数让D3知道应如何在每个时刻绘制每个路径。
该函数的外观如下:
function arcTween(a) {
const i = d3.interpolate(this._current, a);
this._current = i(1);
return t => arc(i(t));
}
我们在这里使用d3.interpolate()
来创建所谓的interpolator 。 当我们调用存储在i
变量中的函数,其值在0到1之间时,我们将获得一个介于this._current
和a
之间的值。 在这种情况下, this._current
是一个对象,其中包含我们正在查看的饼图的开始和结束角度,并且a
表示我们要更新到的新数据点。
设置完插值器后,我们将更新this._current
值以包含结尾处的值( i(a)
),然后返回一个将计算路径的函数。基于此t
值,我们的弧应包含。 我们的过渡将在其时钟的每个刻度上运行此函数(在0和1之间传递参数),并且此代码将意味着我们的过渡将知道在任何时间点应在何处绘制弧。
最后,我们的update()
函数需要添加以前数据数组中没有的新元素:
// ENTER
path.enter().append("path")
.attr("fill", (d, i) => color(i))
.attr("d", arc)
.attr("stroke", "white")
.attr("stroke-width", "6px")
.each(function(d) { this._current = d; });
此代码块将首次运行此更新功能,以设置每个圆弧的初始位置。 这里的enter()
方法为我们提供了数据中需要添加到屏幕上的所有元素,然后我们可以使用attr()
方法遍历每个元素,以设置每个元素的填充和位置弧。 我们还将每个弧线都设置为白色边框,这使我们的图表看起来更加整洁。 最后,我们将每个圆弧的this._current
属性设置为数据中项目的初始值,我们将在arcTween()
函数中使用该arcTween()
。
如果您不能完全了解它的工作原理,请不要担心,因为这是D3中相当高级的主题。 这个库的妙处在于,您无需了解其所有内部工作原理即可使用它创建一些强大的东西。 只要您了解需要更改的部分,就可以抽象出一些并非完全必要的细节。
这将我们带入流程的下一步……
适应代码
既然我们在本地环境中有了一些代码,并且了解了它在做什么,那么我将切换出我们正在查看的数据,从而使其与我们感兴趣的数据一起使用。
我已经将要使用的data/
包含在项目的data/
文件夹中。 由于这次新的incomes.csv
文件为CSV格式(这是您可以使用Microsoft Excel打开的文件),因此我将使用d3.csv()
函数,而不是d3.json()
函数:
d3.csv("data/incomes.csv").then(data => {
...
});
此函数与d3.json()
基本上具有相同的d3.json()
-将我们的数据转换成我们可以使用的格式。 我还将在这里删除type()
初始化函数作为第二个参数,因为这是我们旧数据所特有的。
如果您在d3.csv
回调的顶部添加console.log(data)
语句,您将能够看到我们正在使用的数据的形状:
(50) [{…}, {…}, {…}, {…}, {…}, {…}, {…} ... columns: Array(9)]
0:
1: "12457"
2: "32631"
3: "56832"
4: "92031"
5: "202366"
average: "79263"
top: "350870"
total: "396317"
year: "2015"
1: {1: "11690", 2: "31123", 3: "54104", 4: "87935", 5: "194277", year: "2014", top: "332729", average: "75826", total: "379129"}
2: {1: "11797", 2: "31353", 3: "54683", 4: "87989", 5: "196742", year: "2013", top: "340329", average: "76513", total: "382564"}
...
我们有50个项目的数组,每个项目代表我们数据中的一年。 然后,对于每一年,我们都有一个对象,其中包含五个收入组中每个组的数据以及其他一些字段。 我们可以在这其中的一年中创建一个饼图,但是首先我们需要对数据进行一些调整,以使其格式正确。 当我们想用D3编写数据联接时,我们需要传递一个数组,其中每个项目都将绑定到SVG。
回想一下,在我们的最后一个示例中,我们有一个数组,其中包含要显示在屏幕上的每个饼图切片的项目。 将此与当前的对象进行比较,这是一个对象,其键1到5代表我们要绘制的每个饼图切片。
为了解决这个问题,我将添加一个名为prepareData()
的新函数来替换我们之前拥有的type()
函数,该函数将在加载数据时对每个数据进行迭代:
function prepareData(d){
return {
name: d.year,
average: parseInt(d.average),
values: [
{
name: "first",
value: parseInt(d["1"])
},
{
name: "second",
value: parseInt(d["2"])
},
{
name: "third",
value: parseInt(d["3"])
},
{
name: "fourth",
value: parseInt(d["4"])
},
{
name: "fifth",
value: parseInt(d["5"])
}
]
}
}
d3.csv("data/incomes.csv", prepareData).then(data => {
...
});
每年,此函数都会返回一个带有values
数组的对象,我们将其传递到数据联接中。 我们正在使用name
字段标记这些值中的每一个,并根据已经获得的收入值为其提供数值。 我们还将跟踪每年的平均收入以进行比较。
至此,我们的数据格式可以使用:
(50) [{…}, {…}, {…}, {…}, {…}, {…}, {…} ... columns: Array(9)]
0:
average: 79263
name: "2015"
values: Array(5)
0: {name: "first", value: 12457}
1: {name: "second", value: 32631}
2: {name: "third", value: 56832}
3: {name: "fourth", value: 92031}
4: {name: "fifth", value: 202366}
1: {name: "2014", average: 75826, values: Array(5)}
2: {name: "2013", average: 76513, values: Array(5)}
...
首先,我们将在数据中生成第一年的图表,然后再担心在接下来的几年中对其进行更新。
目前,我们的数据开始于2015年,结束于1967年,因此在执行其他任何操作之前,我们需要反转此数组:
d3.csv("data/incomes.csv", prepareData).then(data => {
data = data.reverse();
...
});
与普通的饼图不同,对于我们的图形,我们要固定每个圆弧的角度,并且随着可视化的更新而改变半径。 为此,我们将在饼图布局上更改value()
方法,以使每个饼图切片始终获得相同的角度:
const pie = d3.pie()
.value(1)
.sort(null);
接下来,每次可视化更新时,我们都需要更新半径。 为此,我们需要提出一个可以使用的比例尺 。 比例尺是D3中的一个函数,它接受两个值之间的输入 ,我们将其作为域传入,然后将两个值之间的输出吐出,将它们作为range传入。 这是我们将要使用的比例尺:
d3.csv("data/incomes.csv", prepareData).then(data => {
data = data.reverse();
const radiusScale = d3.scaleSqrt()
.domain([0, data[49].values[4].value])
.range([0, Math.min(width, height) / 2]);
...
});
只要我们能够访问我们的数据,便会立即添加此比例,这是说我们的输入值应介于0到数据集中的最大值之间,这是数据中去年最富有的群体的收入( data[49].values[4].value
)。 对于域,我们正在设置输出值应介于其之间的间隔。
这意味着输入为零将使我们的像素值为零,而输入数据中的最大值将为我们提供宽度或高度值的一半的值(以较小者为准)。
请注意,这里我们也使用平方根标度 。 我们这样做的原因是我们希望饼图的面积与每个组的收入成正比,而不是半径。 由于面积=πR2,我们需要使用一个平方根规模考虑到这一点。
然后,我们可以使用此比例尺在我们的update()
函数中update()
电弧生成器的outerRadius
值:
function update(value = this.value) {
arc.outerRadius(d => radiusScale(d.data.value));
...
});
每当我们的数据更改时,这将编辑我们要用于每个圆弧的半径值。
最初设置弧发生器时,我们还应该删除对outerRadius
的调用,以便仅将其放在文件顶部:
const arc = d3.arc()
.innerRadius(0);
最后,我们需要对该update()
函数进行一些编辑,以便所有内容都与我们的新数据匹配:
function update(data) {
arc.outerRadius(d => radiusScale(d.data.value));
// JOIN
const path = svg.selectAll("path")
.data(pie(data.values));
// UPDATE
path.transition().duration(200).attrTween("d", arcTween);
// ENTER
path.enter().append("path")
.attr("fill", (d, i) => color(i))
.attr("d", arc)
.attr("stroke", "white")
.attr("stroke-width", "2px")
.each(function(d) { this._current = d; });
}
由于我们不再使用单选按钮,因此我通过调用传递了要使用的Year对象。
// Render the first year in our data
update(data[0]);
最后,我将删除为表单输入设置的事件侦听器。 如果一切都按计划进行,那么我们的数据中的第一年应该有一张漂亮的图表:
使其动态
下一步是使我们的可视化周期在不同年份之间,以显示收入随时间变化的方式。 我们将通过添加对JavaScript的setInterval()
函数的调用来实现此目的,我们可以使用该函数重复执行一些代码:
d3.csv("data/incomes.csv", prepareData).then(data => {
...
function update(data) {
...
}
let time = 0;
let interval = setInterval(step, 200);
function step() {
update(data[time]);
time = (time == 49) ? 0 : time + 1;
}
update(data[0]);
});
我们在此time
变量中设置了一个计时器,每200毫秒,此代码将运行step()
函数,该函数会将图表更新为下一年的数据,并将计时器增加1。值49(我们数据中的最后一年),它将自行重置。 现在,这给了我们一个可以连续运行的不错的循环:
为了使事情更有用。 我还将添加一些标签,为我们提供原始数据。 我将用以下内容替换文件正文中的所有HTML代码:
<h2>Year: <span id="year"></span></h2>
<div class="container" id="page-main">
<div class="row">
<div class="col-md-7">
<div id="chart-area"></div>
</div>
<div class="col-md-5">
<table class="table">
<tbody>
<tr>
<th></th>
<th>Income Bracket</th>
<th>Household Income (2015 dollars)</th>
</tr>
<tr>
<td id="leg5"></td>
<td>Highest 20%</td>
<td class="money-cell"><span id="fig5"></span></td>
</tr>
<tr>
<td id="leg4"></td>
<td>Second-Highest 20%</td>
<td class="money-cell"><span id="fig4"></span></td>
</tr>
<tr>
<td id="leg3"></td>
<td>Middle 20%</td>
<td class="money-cell"><span id="fig3"></span></td>
</tr>
<tr>
<td id="leg2"></td>
<td>Second-Lowest 20%</td>
<td class="money-cell"><span id="fig2"></span></td>
</tr>
<tr>
<td id="leg1"></td>
<td>Lowest 20%</td>
<td class="money-cell"><span id="fig1"></span></td>
</tr>
</tbody>
<tfoot>
<tr>
<td id="avLeg"></td>
<th>Average</th>
<th class="money-cell"><span id="avFig"></span></th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
我们将在这里使用Bootstrap的网格系统来构建页面,该系统可让我们将页面元素整齐地格式化为方框。
每当我们的数据更改时,我都会使用jQuery更新所有这些信息:
function updateHTML(data) {
// Update title
$("#year").text(data.name);
// Update table values
$("#fig1").html(data.values[0].value.toLocaleString());
$("#fig2").html(data.values[1].value.toLocaleString());
$("#fig3").html(data.values[2].value.toLocaleString());
$("#fig4").html(data.values[3].value.toLocaleString());
$("#fig5").html(data.values[4].value.toLocaleString());
$("#avFig").html(data.average.toLocaleString());
}
d3.csv("data/incomes.csv", prepareData).then(data => {
...
function update(data) {
updateHTML(data);
...
}
...
}
我还将在文件顶部对CSS进行一些编辑,这将为我们每个圆弧提供一个图例,并使标题居中:
<style>
#chart-area svg {
margin:auto;
display:inherit;
}
.money-cell { text-align: right; }
h2 { text-align: center; }
#leg1 { background-color: #66c2a5; }
#leg2 { background-color: #fc8d62; }
#leg3 { background-color: #8da0cb; }
#leg4 { background-color: #e78ac3; }
#leg5 { background-color: #a6d854; }
#avLeg { background-color: grey; }
@media screen and (min-width: 768px) {
table { margin-top: 100px; }
}
</style>
我们最终得到的是相当可观的东西:
由于在这里很难看到这些弧线是如何随时间变化的,因此我想添加一些网格线以显示数据第一年的收入分布情况:
d3.csv("data/incomes.csv", prepareData).then(data => {
...
update(data[0]);
data[0].values.forEach((d, i) => {
svg.append("circle")
.attr("fill", "none")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", radiusScale(d.value))
.attr("stroke", color(i))
.attr("stroke-dasharray", "4,4");
});
});
我正在使用Array.forEach()
方法来完成此操作,尽管我也可以再次使用D3的常规“ 常规更新模式” (JOIN / EXIT / UPDATE / ENTER)。
我还想添加一行以显示美国的平均收入,我将每年对其进行更新。 首先,我将首次添加平均线:
d3.csv("data/incomes.csv", prepareData).then(data => {
...
data[0].values.forEach((d, i) => {
svg.append("circle")
.attr("fill", "none")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", radiusScale(d.value))
.attr("stroke", color(i))
.attr("stroke-dasharray", "4,4");
});
svg.append("circle")
.attr("class", "averageLine")
.attr("fill", "none")
.attr("cx", 0)
.attr("cy", 0)
.attr("stroke", "grey")
.attr("stroke-width", "2px");
});
然后,当年份更改时,我将在update()
函数末尾更新此内容:
function update(data) {
...
svg.select(".averageLine").transition().duration(200)
.attr("r", radiusScale(data.average));
}
我应该注意,对我们来说, 在第一次调用update()
之后添加每个圆非常重要,因为否则它们最终将被渲染到每个圆弧路径的后面 (SVG层由它们的顺序决定。重新添加到屏幕上,而不是按其z-index)。
此时,我们可以更清楚地传达正在处理的数据:
使其互动
最后,我希望我们添加一些控件,以使用户深入了解特定的年份。 我要添加“ 播放/暂停”按钮以及“年份”滑块,以允许用户选择要查看的特定日期。
这是用于将这些元素添加到屏幕上HTML:
<div class="container" id="page-main">
<div id="controls" class="row">
<div class="col-md-12">
<button id="play-button" class="btn btn-primary">Play</button>
<div id="slider-div">
<label>Year: <span id="year-label"></span></label>
<div id="date-slider"></div>
</div>
</div>
</div>
...
</div>
我们需要为这两个元素添加一些事件侦听器,以设计我们正在寻找的行为。
首先,我要定义“ 播放/暂停”按钮的行为。 我们需要替换之前为时间间隔编写的代码,以允许我们使用按钮停止和启动计时器。 我假设可视化以“已暂停”状态开始,并且我们需要按此按钮才能开始。
function update(data) {
...
let time = 0;
let interval;
function step() {
update(data[time]);
time = (time == 49) ? 0 : time + 1;
}
$("#play-button").on("click", function() {
const button = $(this);
if (button.text() === "Play"){
button.text("Pause");
interval = setInterval(step, 200);
} else {
button.text("Play");
clearInterval(interval);
}
});
...
}
每当我们的按钮被点击时,我们的if/else
块将根据我们的按钮是“播放”按钮还是“暂停”按钮来定义不同的行为。 如果要单击的按钮显示为“播放”,则将其更改为“暂停”按钮,然后开始进行间隔循环。 或者,如果该按钮是“暂停”按钮,则将其文本更改为“播放”,并使用clearInterval()
函数停止循环的运行。
对于我们的滑块,我想使用jQuery UI库附带的滑块。 我将其包含在我们HTML中,并准备编写几行以将其添加到屏幕中:
function update(data) {
...
$("#date-slider").slider({
max: 49,
min: 0,
step: 1,
slide: (event, ui) => {
time = ui.value;
update(data[time]);
}
});
update(data[0]);
...
}
在这里,我们使用slide
选项将事件侦听器附加到滑块。 每当滑块移动到另一个值时,我们都会将计时器更新为该新值,并在当年在数据中运行update()
函数。
我们可以在update()
函数的末尾添加此行,以便在循环运行时,滑块移至正确的年份:
function update(data) {
...
// Update slider position
$("#date-slider").slider("value", time);
}
我还将在我们的updateHTML()
函数中添加一行(只要我们的可视化发生变化,该函数就会运行),该函数可以根据数据中的当前年份来调整标签的值:
function updateHTML(data) {
// Update title
$("#year").text(data.name);
// Update slider label
$("#year-label").text(data.name);
// Update table values
$("#fig1").html(data.values[0].value.toLocaleString());
...
}
我将在CSS中添加更多行,以使所有内容看起来更加整洁:
<style>
...
@media screen and (min-width: 768px) {
table { margin-top: 100px; }
}
#page-main { margin-top: 10px; }
#controls { margin-bottom: 20px; }
#play-button {
margin-top: 10px;
width: 100px;
}
#slider-div {
width:300px;
float:right;
}
</style>
我们拥有了它-我们的最终产品-一个功能完备的交互式数据可视化工具,一切都按预期工作。
希望本教程演示了D3的真正功能,使您可以创建可以想象的任何东西。
从头开始使用D3始终是一个艰难的过程,但是回报是值得的。 如果您想学习如何创建自己的自定义可视化效果,以下一些在线资源可能会有所帮助:
- SitePoint的D3.js内容概述。
- D3主页上的库简介 。 这贯穿了一些最基本的命令,向您展示了如何在D3中进行前几步。
- D3的创建者Mike Bostock撰写的“ 让我们制作条形图 ”向初学者展示如何制作图书馆中最简单的图形之一。
- Elijah Meeks撰写的D3.js in Action (35美元),这是一本扎实的入门教材,其中涉及很多细节。
- D3的Slack频道非常欢迎D3的新手。 它还有一个“学习资料”部分,其中包含大量资源。
- 此在线Udemy课程 (20美元),通过一系列视频讲座涵盖了图书馆中的所有内容。 这是针对JavaScript开发人员的,包括四个不错的项目。
- 可以从bl.ocks.org和blockbuilder.org获得大量示例可视化。
- D3 API参考 ,其中提供了D3必须提供的所有内容的详尽的技术说明。
而且不要忘了,如果您想查看本文中使用的代码的完成版本,那么可以在我们的GitHub repo上找到它 。
翻译自: https://www.sitepoint.com/interactive-data-visualization-javascript-d3/
javascript可视化