常用git的同学可能对热图(heatmap)已经很熟悉了,就是这个东西

我的contribution真是丢人,希望之后可以填的更满一点

现在我们来做一个自己的heatmap,当然还是熟悉的那个数据

预处理

先把数据读入,然后做一些预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
const pathToJSON = "./data/nyc_weather_data.json"
let dataset = await d3.json(pathToJSON)

const parseDate = d3.timeParse("%Y-%m-%d")
const dateAccessor = d => parseDate(d.date)
dataset = dataset.sort((a, b) => dateAccessor(a) - dateAccessor(b))

const firstDate = dateAccessor(dataset[0])

const weekFormat = d3.timeFormat("%-e")
const xAccessor = d => d3.timeWeeks(firstDate, dateAccessor(d)).length
const dayOfWeekFormat = d3.timeFormat("%-w")
const yAccessor = d => +dayOfWeekFormat(dateAccessor(d))

之后计算一些参数,我们期望的是一列显示一周七天,所以要先算出星期的数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const numberOfWeeks = Math.ceil(dataset.length / 7) + 1
let dimensions = {
margin: {
top: 30,
right: 0,
bottom: 0,
left: 80,
},
}
dimensions.width = (window.innerWidth
- dimensions.margin.left
- dimensions.margin.right) * 0.95
dimensions.boundedWidth = dimensions.width
- dimensions.margin.left
- dimensions.margin.right
dimensions.height =
dimensions.boundedWidth * 7 / numberOfWeeks
+ dimensions.margin.top
+ dimensions.margin.bottom
dimensions.boundedHeight = dimensions.height
- dimensions.margin.top
- dimensions.margin.bottom

和往常一样建立画布

1
2
3
4
5
6
7
8
9
const wrapper = d3.select("#wrapper")
.append("svg")
.attr("width", dimensions.width)
.attr("height", dimensions.height)

const bounds = wrapper.append("g")
.style("transform", `translate(
${dimensions.margin.left}px, ${dimensions.margin.top}px
)`)

还需要计算的参数是表示每天的每个小格子的长宽以及格子的间隙

1
2
3
4
5
6
const barPadding = 5
const totalBarDimension = d3.min([
dimensions.boundedWidth / numberOfWeeks,
dimensions.boundedHeight / 7,
])
const barDimension = totalBarDimension - barPadding

绘制图表

先把月份的信息绘制出来

1
2
3
4
5
6
7
8
9
10
const monthFormat = d3.timeFormat("%b")
const months = bounds.selectAll(".month")
.data(d3.timeMonths(dateAccessor(dataset[0]),
dateAccessor(dataset[dataset.length - 1])))
.enter().append("text")
.attr("class", "month")
.attr("transform", d => `translate(${
totalBarDimension * d3.timeWeeks(firstDate, d).length}, -10
)`)
.text(d => monthFormat(d))

d3.timeMonths可以将起止时间按照月份间隔划分生成数据,生成的数据输出如下

绘制的效果如下

星期的绘制如法炮制

1
2
3
4
5
6
7
8
const dayOfWeekParse = d3.timeParse("%-e")
const dayOfWeekTickFormat = d3.timeFormat("%-A")
const labels = bounds.selectAll(".label")
.data(new Array(7).fill(null).map((d, i) => i))
.enter().append("text")
.attr("class", "label")
.attr("transform", d => `translate(-10, ${totalBarDimension * (d + 0.5)})`)
.text(d => dayOfWeekTickFormat(dayOfWeekParse(d)))

然后我们来画里边的格子,其实就是在指定的位置画上对应的小方块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建颜色比例尺
const colorAccessor = d => d[metric]
const colorRangeDomain = d3.extent(dataset, colorAccessor)
const colorRange = d3.scaleLinear()
.domain(colorRangeDomain)
.range([0, 1])
.clamp(true)
const colorGradient = d3.interpolateHcl("#ecf0f1", "pink")
const colorScale = d => colorGradient(colorRange(d))

const days = bounds.selectAll(".day")
.data(dataset, d => d.date)

const newDays = days.enter().append("rect")
const allDays = newDays.merge(days)
.attr("class", "day")
.attr("x", d => totalBarDimension * xAccessor(d))
.attr("width", barDimension)
.attr("y", d => totalBarDimension * yAccessor(d))
.attr("height", barDimension)
.style("fill", d => colorScale(colorAccessor(d)))

最后再加上一个标题和比例尺标注

1
2
3
4
5
6
7
8
9
10
11
12
d3.select("#metric")
.text(metric)
d3.select("#legend-min")
.text(colorRangeDomain[0])
d3.select("#legend-max")
.text(colorRangeDomain[1])
d3.select("#legend-gradient")
.style("background", `linear-gradient(to right, ${
new Array(10).fill(null).map((d, i) => (
`${colorGradient(i / 9)} ${i * 100 / 9}%`
)).join(", ")
})`)

交互部分则沿用之前柱状图中的按钮来切换数据,热图就画完了

[演示地址]