简介
现在我们已经创建了我们的第一个图表,让我们创建另一个更复杂的图表。在本章的最后,我们将对在d3中制作图表所需的每个步骤有更深入的理解。 我们可以对我们的天气数据集提出无穷的问题-他们中的许多人询问不同指标之间的关系。让我们来研究一下这两个指标:
- dew point(露珠) 露珠形成的最高温度(°F)
- humidity(湿度) 是指空气中的水蒸气的量 我认为它们是相关的-高湿度应该会导致更高的露点温度,对吧?让我们深入研究看看吧!
决定图表类型
当观察两个指标之间的关系时,散点图是一个不错的选择。
散点图包括两个轴x轴和y轴分别表示dew point 和 humidity
我们将把每个数据点(在这种情况下,只有一天)绘制为一个点。如果我们想涉及第三个指标,我们甚至可以通过改变每个点的颜色或大小来添加另一个维度。
散点图的伟大之处在于,当我们完成绘制图表时,我们将清楚地看到这两个指标之间的关系。我们将在第八章中进一步讨论潜在的模式。
在绘制任何图表时所执行的步骤
在d3中,我们每次制作图表都需要采取一些步骤的一般步骤——我们在第一章中简要介绍了每个步骤来创建我们的折线图,但现在让我们创建一个检查表,为我们提供一个未来图表的路线图。
-
获取数据 查看数据结构,并声明如何访问我们所需要的值
-
创建图表尺寸 表示物理(即像素)图表参数
-
绘制画布 渲染图表区域和边界元素
-
创建刻度尺 为图表中每一个data-to-physical 属性创建刻度尺
-
绘制数据 渲染数据元素
-
绘制外围信息 渲染轴、标签和图例
-
设置交互作用 初始化事件侦听器并创建交互行为——我们将在第5章中获得这个步骤
开始绘图
存取数据
import数据 x轴获取成雾温度 y轴获取当天湿度
import { weatherData, DataType } from './data-interface'
const xAccessor = (d: weatherData) => d.dewPoint
const yAccessor = (d: weatherData) => d.humidity
创建图表尺寸
接下来,我们需要定义图表的维度。通常,散点图是正方形的,x轴宽和y轴一样高。这使得一旦数据点通过不拉伸或挤压其中一个尺度来绘制出来,就可以更容易地查看它们的整体形状 我将组件封装起来wapper使用100%可用宽高,使用svg的viewBox属性来设置宽高 文章中有一个讨论点使用了
d3.min([
window.innerWidth * 0.9,
window.innerHeight * 0.9,
])
求最小值以此来保持图不超出屏幕,为什么不用Math.min:
- Math.min将数组中的任何nulls或undefineds计算为0,而d3.min将忽略它们
- 如果数组中有一个值不能转换为一个数字,则Math.min将返回NaN,而d3.min将忽略它
- 如果我们需要一个accessor函数d3.min 将会组织创建另一个新数组
- 如果数据集为空,Math.min将返回Infinity,而d3.min将返回undefined
- Math.min使用数字顺序,而d3.min使用自然顺序,这允许它处理字符串 所以结论就是在d3中的数据处理最好使用d3的函数
const dimensions = {
viewBox: '0, 0, 400,400',
margin: {
top: 10,
right: 10,
bottom: 50,
left: 50,
},
boundedWidth: 340,
boundedHeight: 340,
}
绘制画布
和上一个图一样我们使用d3获取wapper元素,然后添加svg,为svg设置宽高,在svg下添加bounds设置宽高并设置偏移量
const wapper = d3.select('#demo-scatterplot-wapper')
const svg = wapper.append('svg').attr('width', '100%').attr('height', '100%').attr('viewBox', dimensions.viewBox)
const bounds = svg.append('g').style('transform', `translate(${dimensions.margin.left}px,${dimensions.margin.top}px)`)
创建刻度尺
在绘制数据之前,我们必须弄清楚如何将数字从数据域转换为像素域。
让我们从x轴开始。我们想根据每天的点的露点来决定它的水平位置。
为了找到这个位置,我们使用一个d3.scale的对象,它可以帮助我们将数据映射到像素。让我们创建一个需要一个露点(温度)的尺度,并告诉我们一个点需要在右边多远处。
这将是一个线性尺度,因为输入(露点临界温度)和输出(像素)将是线性增加的数字
const xScale = d3
.scaleLinear()
.domain(d3.extent(dataset, xAccessor) as Iterable<number>)
.range([0, dimensions.boundedWidth])
.nice()
const yScale = d3
.scaleLinear()
.domain(d3.extent(dataset, yAccessor) as Iterable<number>)
.range([dimensions.boundedWidth, 0])
.nice()
在这个比例尺中,我们的domin中的数据最大最小值必然会有贴合在x轴或者y轴上,我们可以用d3-scale 提供了.nice()方法对我们的比例尺进行优化,对比如下
绘制数据
有趣的部分来了!绘制散点图的点将不同于我们绘制时间轴的方式。还记得我们用一条线覆盖了所有的数据点吗?对于我们的散点图,我们需要每个数据点有一个元素
我们将希望使用<circle>SVG元素,谢天谢地的是,它不需要d属性字符串。相反,我们将给它cx和cy属性,它们分别设置它的x和y坐标。这些位置是圆的中心,r属性设置圆的半径(其宽度或高度的一半)。
bounds
.append('circle')
.attr('cx', dimensions.boundedWidth / 2)
.attr('cy', dimensions.boundedHeight / 2)
.attr('r', 5)
上面的代码会在图中心画出一个圆点
学习d3需要有svg基础,以下是常用属性:
这些圆点需要x,y坐标
dataset.forEach(d => {
bounds
.append("circle")
.attr("cx", xScale(xAccessor(d)))
.attr("cy", yScale(yAccessor(d)))
.attr("r", 5)
})
虽然这种绘制点的方法目前还有效,但我们应该解决几个问题。
- 我们添加了一个嵌套级别,这使得我们的代码更难遵循。
- 如果我们运行两次这个函数,我们最终会绘制两组点。当我们开始更新我们的图表时,我们将希望用相同的代码绘制和更新我们的数据,以防止重复我们自己。为了解决这些问题并保持我们的代码干净,让我们处理而不使用这些点 为了解决这些问题并保持我们的代码干净,让我们不使用循环来处理这些点。
Data joins
划掉最后一块代码。D3的函数将帮助我们解决上述问题。 我们将从抓取d3选择对象中的所有元素开始。我们将不再使用d3.selection.select()方法,它返回一个匹配元素,而是使用它的.selectAll()方法,它返回一个匹配元素数组。
const dots = bounds.selectAll("circle")
这从一开始看起来很奇怪——我们还没有任何点,为什么我们要选择一些不存在的东西呢?别担心!你很快就会适应这种模式了。
我们正在创建一个d3.selection,它知道什么元素已经存在。如果我们已经绘制了数据集的一部分,那么这个选择将知道已经绘制了哪些点,以及需要添加哪些点。
bounds.selectAll('circle').data(dataset)
当我们调用.data()方法,我们将我们选择的元素与我们的数据点数组连接起来。 返回的selection将包含一个现有元素、需要添加的新元素和需要删除的旧元素的列表。
我们将以三种方式看到我们的选择对象的这些变化:
- 我们的选择对象被更新,以包含现有DOM元素和数据点之间的任何重叠
- 添加了一个_enter键,它列出了尚未渲染元素的任何数据点
- 添加了一个_exit键,它列出了已经呈现但没有在提供的数据集中的任何数据点
让我们通过将更新后的选择对象记录到控制台来了解它是什么样子的。
let dots = bounds.selectAll("circle")
console.log(dots)
dots = dots.data(dataset)
console.log(dots)
请记住,当前选择的DOM元素位于_groups键下
但是,下一个选择对象看起来会有所不同。我们有两个新键:_enter和_exit,我们的_groups数组有一个包含365个元素的数组
让我们仔细看看_enter键。如果我们展开数组并查看其中一个值,我们可以看到一个具有__data__属性的对象。
如果我们点开__data__值,我们将看到一个我们的数据点。 太棒了!我们可以看到,_enter中的每个值都对应于我们的数据集中的一个值。这是我们所期望的,因为所有的数据点都需要添加到DOM中。
_exit值是一个空数组——如果我们删除现有的元素,我们将看到这里列出的那些元素。
为了对新元素起作用,我们可以使用enter方法创建一个只包含这些元素的d3 selection对象。当我们在第4章中进行转换时,我们需要一个针对旧元素的匹配方法(exit)。
让我们更好地看看那个新的选择对象:
const dots = bounds.selectAll("circle")
.data(dataset)
.enter()
console.log(dots)
这看起来和我们之前操作过的任何d3 selection 对象一样。让我们为每个数据点附加一个<circle>。我们可以使用与我们对单节点选择对象使用的append()方法相同的.append()方法,d3将为每个数据点创建一个元素。
const dots = bounds
.selectAll('circle')
.data(dataset)
.enter()
.append('circle')
.attr('cx', (d) => xScale(xAccessor(d)))
.attr('cy', (d) => yScale(yAccessor(d)))
.attr('r', 5)
数据加入练习
这里有一个快速的例子来帮助可视化数据连接的概念。我们将把数据集分成两部分,并分别绘制这两个部分。暂时注释您完成的点代码,所以我们重新开始工作。我们完成这个练习后再把它放回去。
让我们添加一个名为drawDots()的函数,它模拟我们的点绘图代码。此函数将选择所有现有的圆,与提供的数据集连接,并用提供的颜色绘制任何新圆。
function drawDots(dataset, color) {
const dots = bounds.selectAll("circle").data(dataset)
dots
.enter().append("circle")
.attr("cx", d => xScale(xAccessor(d)))
.attr("cy", d => yScale(yAccessor(d)))
.attr("r", 5)
.attr("fill", color)
}
让我们用我们的数据集的一部分来调用这个函数。颜色并不重要,让我们选择深灰色。 drawDots(dataset.slice(0,200),“darkgrey”)我们应该看到页面上画的一些点。
一秒钟后,让我们用整个数据集再次调用这个函数,这次是用蓝色。我们增加了一个超时时间来帮助区分这两组点。
setTimeout(() => {
drawDots(dataset,
"cornflowerblue")
}, 1000)
当你刷新你的网页时,你应该看到一组灰点,一秒钟后再看到一组蓝点
每次我们运行drawDots(),我们只设置新圆的颜色。这就解释了为什么灰点会保持灰色。如果我们想设置所有圆的颜色,我们可以重新选择所有圆,并在新选择上设置它们的填充:
function drawDots(dataset, color) {
const dots = bounds.selectAll("circle").data(dataset)
dots.enter().append("circle")
bounds.selectAll("circle")
.attr("cx", d => xScale(xAccessor(d)))
.attr("cy", d => yScale(yAccessor(d)))
.attr("r", 5)
.attr("fill", color)
}
为了保持链的运行,d3选择对象有一个merge()方法,该方法将把当前的选择与另一个选择结合起来。在这种情况下,我们可以将新的输入选择与原始的点选择结合起来,这将返回完整的点列表。当我们为新的合并选择设置属性时,我们将更新所有的点。
function drawDots(dataset, color) {
const dots = bounds.selectAll("circle").data(dataset)
dots
.enter().append("circle")
.merge(dots)
.attr("cx", d => xScale(xAccessor(d)))
.attr("cy", d => yScale(yAccessor(d)))
.attr("r", 5)
.attr("fill", color)
}
.join()
自从d3-selection1.4.0版本,有一个新的.join()方法可以帮助减少这段代码加.enter(), .append(),.merge(),这允许我们编写以下代码:
function drawDots(dataset, color) {
const dots = bounds.selectAll("circle").data(dataset)
dots.join("circle")
.attr("cx", d => xScale(xAccessor(d)))
.attr("cy", d => yScale(yAccessor(d)))
.attr("r", 5)
.attr("fill", color)
}
虽然。join()是对d3的一个很好的补充,但理解.enter()、.append()和.merge()方法仍然是有益的。大多数现有的d3代码都会使用这些方法,在开始之前了解基本知识是很重要的。现在,让我们删除这个示例代码,取消注释我们完成的点代码,并继续使用我们的散点图!
绘制外围
x轴:
- 横跨底部的一条线
- 带有间隔标记
- 每个间隔的值的标签
- 一个轴的整体标签 为此,我们将使用d3.axisBottom()创建我们的轴生成器,然后传递它:
- 我们的x刻度尺,这样它知道哪里出现刻度(通过domain)
- 尺寸是多少(通过range)
const xAxisGenerator = d3.axisBottom(xScale)
接下来,我们将使用xAxisGenerator(),并在一个新的g元素上调用它。记住,我们需要转换x轴来将其移动到图表边界的底部。
const xAxis = bounds.append('g').call(xAxisGenerator).style('transform', `translate(0,${dimensions.boundedHeight}px)`)
让我们扩展我们的知识,并为我们的轴创建标签。在SVG中绘制文本相当简单——我们需要一个<text>元素,它可以用x和y属性来定位。我们想把它水平的中心,略高于图表的底部。
<text>元素将它们的子元素显示为文本——我们可以用我们选择器的.html()方法来设置它
我们需要显式地将文本填充设置为黑色,因为它继承了轴<g>元素设置的无填充值。
const xAxis = bounds
.append('g')
.call(xAxisGenerator)
.style('transform', `translate(0,${dimensions.boundedHeight}px)`)
.append('text')
.attr('x', dimensions.boundedWidth / 2)
.attr('y', dimensions.margin.bottom - 10)
.attr('fill', 'black')
.style('font-size', '1.4em')
.text('Dew point (℉)')
y轴: 同x轴一样,但是我们希望只显示四个间隔,这样已经给用户足够的信息。
注意,得到的轴不一定恰好有4个刻度。D3将把这个数字作为一个建议,并计划显示那么些刻度,但也试图使用友好的间隔。看看d3-array—中的一些内部逻辑,看看它是如何尝试使用间隔10,然后是5,然后是2?
有很多方法可以配置d3轴的tick——在文档中找到它们.例如,您可以通过将值数组传递给.tickValues()来指定它们的精确值。
github.com/d3/d3-axis#… 让我们用生成器来绘制y轴:
const yAxisGenerator = d3.axisLeft(yScale).ticks(4)
const yAxis = bounds
.append('g')
.call(yAxisGenerator)
.append('text')
.attr('x', -dimensions.boundedHeight / 2)
.attr('y', -dimensions.margin.left + 10)
.attr('fill', 'black')
.style('font-size', '1.4em')
.text('Relative humidity')
.style('transform', 'rotate(-90deg)')
.style('text-anchor', 'middle')
我们需要旋转这个标签以适应y轴的旁边。要围绕它的中心旋转它,我们可以将它的CSS属性文本锚定设置为中间。
总结
现在我们已经绘制完了散点图,我们可以后退一步,看看我们可以从以这种方式显示数据中学到什么。如果不进行统计分析(如皮尔逊相关系数或相互分析),我们将无法就我们的指标是否具有相关性做出任何明确的观点。然而,我们仍然可以了解它们是如何相互联系的。看看所绘制的点,它们似乎确实从图表的左下角到右上方围绕着一条看不见的线排列。
一般来说,我们猜测高湿度可能符合高露点临界值似乎是正确的。我们将讨论我们在第10章的散点图中可能看到的不同类型的模式。
还记得我说过我们可以引入另一个度量标准吗?云覆盖量-让我们通过添加一个颜色尺度来展示云覆盖量如何根据湿度和露点临界温度而变化
const colorScale = d3
.scaleLinear()
.domain(d3.extent(dataset, yAccessor) as Iterable<number>)
.range(['skyblue', 'darkslategrey'] as Iterable<number>)
const dots = bounds
.selectAll('circle')
.data(dataset)
.join('circle')
.attr('cx', (d) => xScale(xAccessor(d)))
.attr('cy', (d) => yScale(yAccessor(d)))
.attr('r', 5)
.attr('fill', (d) => colorScale(colorAccessor(d)))
对于一个完整的、可访问的图表,最好添加一个图例来解释我们的颜色是什么意思。请继续关注!我们将在第6章学习如何添加色比例图例。
--下一个图-柱状图