前言
我们将完成最后一个基本图表--一旦完成,你会对每一步都感觉非常舒服,我们将转向更令人兴奋的概念,如动画和互动。
决定图表类型
决定图表类型我们可以问数据集的另一种问题是:指标的分布是什么样的?例如:
- 我们有什么样的湿度值?
- 湿度水平通常保持在一个值左右,几天非常潮湿,几天非常干燥?
- 还是它的变化一致,没有标准值? 看看我们刚刚绘制的散点图,我们可以从这些点的垂直位置看到每日湿度值。
但这很难回答我们的问题——我们的大部分点都落在图表的中间位置吗?我们不完全确定。
直方图
直方图是一个柱形图,它显示了一个度量的分布,度量值在x轴上,值的频率在y轴上。
为了显示频率,值被放置在相同大小的箱子中(可视化为单独的条形图)。例如,我们可以为露点温度间隔为10度的箱子 - 制作——这些看起来就像[0-10,10-20,20-30,…]。第二个箱的露点为10-20。
箱子的数量和大小取决于实现-你可以有一个只有3个箱子或一个有100个箱子的直方图!有一些可以遵循的标准(请随意查看d3的内置的formulas),但我们通常可以根据什么适合数据和什么容易阅读的数据来决定数字。
我们的目标是制作一个湿度值的直方图。这将向我们展示湿度值的分布,并帮助回答我们的问题。大多数日子都会保持在相同的湿度水平附近吗?还是有两种类型的日子:潮湿和干燥?有疯狂潮湿的日子吗?
为了解释上述直方图,它显示我们的数据集中有48天的时间,湿度值在0.55到0.6之间
为了更加相信,我们将推广我们的直方图函数,并循环通过我们的数据集中的八个度量标准——创建许多直方图来进行比较!
图表清单
首先,让我们来看看我们的图表制作清单,以提醒自己必要的步骤:
- 访问数据
- 创建尺寸标准
- 绘制画布
- 创建刻度尺
- 绘制数据
- 绘制外围数据
- 设置交互作用 我们将轻松地通过这些步骤,加强我们已经学到的东西。
访问数据
这次,我们只对整个图表的一个指标感兴趣。记住,y轴是绘制值在x轴上的度量的频率(即出现次数)。因此,我们定义了一个metricAccessor(),而不是xAccessor()和yAccessor()。
const metricAccessor = (d: weatherData) => d.humidity
创建尺寸
当直方图长比宽更长时,它们最容易阅读。
const dimensions = {
viewBox: '0,0,600,400',
margin: {
top: 30,
right: 10,
bottom: 50,
left: 50,
},
boundsWidth: 540,
boundsHeight: 320,
}
记住,我们的包装器包含了整个图表。如果我们减去边距,我们将得到包含所有数据元素的边界的大小。
绘制canvas
和之前一样
const svg = d3.select('#deom-bar-chart1').append('svg').attr('viewBox', dimensions.viewBox)
const bounds = svg.append('g').style('transform', `translate(${dimensions.margin.left}px,${dimensions.margin.top}px)`)
创建刻度尺
我们的x轴依然是一个线性分割,用.nice()来确保所有数据都在bounds内。
const xScale = d3
.scaleLinear()
.domain(d3.extent(dataset, metricAccessor) as Iterable<number>)
.range([0, dimensions.boundsWidth])
.nice()
现在我们需要创建我们的yScale。但等一会儿!如果不知道我们需要覆盖的频率范围,我们就不能做一个y尺度。让我们先创建我们的数据箱。
创建bins
我们可以使用d3-array的d3.bin()方法来创建一个bin生成器。这个生成器将把我们的数据集转换成一个箱子的数组-我们甚至可以选择多少个我们想要的箱子!让我们来创建一个新的生成器. 保持和x刻度尺域相同 .domain(xScale.domain() as [number, number])
在数据集为对象的情况下,使用.value(d=>d.p) 来生成特定属性分类的箱子
.thresholds(12) 理解为分割次数 参数 d3会根据数字参数进行调整 ,自定义数组参数为固定显示
const binsGenerator = d3
.bin()
.domain(xScale.domain() as [number, number])
.value(metricAccessor as any)
.thresholds(12)
参数[0.3, 0.5, 0.8, 0.9, 1]
参数 12
传入数据生成bins
const bins: any = binsGenerator(dataset as any)
每个箱子都是一个具有以下结构的数组:
- 匹配到的数据数组
- x1 包含 湿度的下限
- x2 不包含 湿度的上限
创建y轴刻度尺
y轴的数据是bins中每个范围内数据的长度,domin中的数组为0-bins中分组数组的最大长度
const yAccessor = (d: []): number => d.length
const yScale = d3
.scaleLinear()
.domain([0, d3.max(bins, yAccessor) as number])
.range([dimensions.boundsHeight, 0])
.nice()
绘制数据
让我们首先创建一个<g>元素来包含我们的箱子。这将有助于保持我们的代码组织,并在DOM中隔离我们的bar,由于现在我们要将每一个bin绘制成长方形,所以我们按照上一章的方法.selectAll('g').data(bins).join('g')即可生成bins的外层g标签。
然后我们在g标签下添加rect:
- x rect的起点x坐标,通过xScale(d.x0)获取当前bin的左侧值
- y rect的起点y坐标 刻度尺参数为当前bin的长度
- width rect的宽度 ,我们需要从bar右侧的x1位置中减去bar左侧的x0位置我们需要从总宽度中减去条形图填充来解释条形图之间的空格。有时我们会得到一个宽度为0的条,然后减去barPadding会小于0,为了防止这种情况发生我们使用d3.max([0, width]).
- height rect的高度 我们将通过从y轴的底部减去y值来计算条的高度。因为我们的y轴从0开始,所以我们可以使用我们的边界高度。
const binsGroups = bounds.append('g').selectAll('g').data(bins).join('g')
const barPadding = 1 // 柱状图间隔
const barRect = binsGroups
.append('rect')
.attr('x', (d: any) => xScale(d.x0))
.attr('y', (d: any) => yScale(d.length))
.attr('width', (d: any): number => d3.max([0, xScale(d.x1) - xScale(d.x0) - barPadding]) as number)
.attr('height', (d: any) => dimensions.boundsHeight - yScale(d.length))
.style('fill', '#cccccc')
添加label
我们想要在每个bar上显示当前bar的数量 数字在bar的上方,为了让数字居中我们使用xScale(d.x0) + (xScale(d.x1) - xScale(d.x0)) / 2) 左侧坐标+bar长度的二分之一 然后将'text-anchor'设置为'middle',text数据使用yAccessor返回长度
const barText = binsGroups
.append('text')
.attr('x', (d: any) => xScale(d.x0) + (xScale(d.x1) - xScale(d.x0)) / 2)
.attr('y', (d: any) => yScale(d.length) - 5)
.style('text-anchor', 'middle')
.text(yAccessor as any)
画一条均线
d3.mean方法可以求出数据的平均值
const mean = d3.mean(dataset, metricAccessor)
const meanLine = bounds
.append('line')
.attr('x1', xScale(mean))
.attr('x2', xScale(mean))
.attr('y1', -15)
.attr('y2', dimensions.boundsHeight)
.attr('stroke', 'maroon')
.attr('stroke-dasharray', '2px 4px')
绘制轴线
由于我们已经给出了每个bar的数值 所以我们没必要画y轴了 我们开始画x轴
const xAxisGender = d3.axisBottom(xScale)
const xAxis = bounds.append('g').call(xAxisGender).style('transform', `translate(0,${dimensions.boundsHeight}px)`)
最后让我们给x轴下加一个标签,来说明x轴的刻度表示的是什么。
const xAxisLabel = bounds
.append('text')
.text('Humidity')
.attr('x', dimensions.boundsWidth / 2)
.attr('y', dimensions.boundsHeight + 30)
.style('font-size', '12px')
我们的直方图看起来介于正态分布和双峰分布之间。如果这些术语现在没有意义,请不要担心——我们将在第8章中详细介绍分布形状。如果你使用你自己的天气数据,你的直方图可能会有一个非常不同的形状。