这次来画一个散点图,用的还是上次画折线图的数据
[数据下载地址]
准备工作
读入数据的方式和之前是一样的
1
| const dataset = await d3.json("./data/nyc_weather_data.json")
|
这次采用湿度和露点作为两个属性
1 2
| const xAccessor = d => d.dewPoint const yAccessor = d => d.humidity
|
我们准备画一个正方形的散点图,所以我们现在计算它的边长
1 2 3 4
| const width = d3.min([ window.innerWidth * 0.9, window.innerHeight * 0.9, ])
|
同样的,设置一个参数表,并计算bound的高和宽
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| let dimensions = { width: width, height: width, margin: { top: 10, right: 10, bottom: 50, left: 50, }, } dimensions.boundedWidth = dimensions.width - dimensions.margin.left - dimensions.margin.right dimensions.boundedHeight = dimensions.height - dimensions.margin.top - dimensions.margin.bottom
|
之后我们用这些参数来创建画布,然后创建bound并平移
1 2 3 4 5 6 7 8 9 10
| 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 7 8
| const xScale = d3.scaleLinear() .domain(d3.extent(dataset, xAccessor)) .range([0, dimensions.boundedWidth]) .nice() const yScale = d3.scaleLinear() .domain(d3.extent(dataset, yAccessor)) .range([dimensions.boundedHeight, 0]) .nice()
|
.nice()做的是一个舍入操作,能够让比例尺范围为最小刻度的整数倍
绘制数据
既然是散点图,我们肯定希望对每个数据绘制一个点,这里我们用小半径的圆来表示点
1 2 3 4 5 6 7
| dataset.forEach(d => { bounds .append("circle") .attr("cx", xScale(xAccessor(d))) .attr("cy", yScale(yAccessor(d))) .attr("r", 5) })
|
如果我们用js的方式来处理,直接用循环把每个点画上去就ok了,但是我们是准备用d3来做数据可视化的,那么就需要考虑到数据更新,需要将图像和数据绑定
d3中的数据状态如下图所示
- enter状态:表示没有绑定元素的数据
- group状态:元素和数据一一对应
- exit状态:没有绑定数据的元素
我们用.dataset()来使得元素和数据关联,之后就可以对于不同状态的数据或元素做对应的处理
1 2 3 4 5 6 7
| 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) .attr("fill", "cornflowerblue")
|
对于enter状态的所有数据,绑定一个图像元素
那如果我既要加入数据,又想要已有的数据一起更新呢,这里就可以用一个merge,将数据合并
1 2 3 4 5 6 7
| 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", "grey")
|
数据合并还有一种更方便的方式,不需要去先找到enter态,然后append,最后merge,而是可以直接使用join
1 2 3 4 5
| dots.join("circle") .attr("cx", d => xScale(xAccessor(d))) .attr("cy", d => yScale(yAccessor(d))) .attr("r", 5) .attr("fill", "grey")
|
创建坐标轴的方式和之前的类似
1 2 3 4 5 6 7 8 9
| const xAxisGenerator = d3.axisBottom().scale(xScale) const xAxis = bounds.append("g") .call(xAxisGenerator) .style("transform", `translateY(${dimensions.boundedHeight}px)`) const yAxisGenerator = d3.axisLeft() .scale(yScale) .ticks(4) const yAxis = bounds.append("g") .call(yAxisGenerator)
|
这回我们还希望把坐标轴对应的维度变量名也表示在图表上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const xAxisLabel = xAxis.append("text") .attr("x", dimensions.boundedWidth / 2) .attr("y", dimensions.margin.bottom - 10) .attr("fill", "black") .style("font-size", "1.4em") .html("Dew point (°F)") const yAxisLabel = yAxis.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")
|
字体大小设置为标准的1.4倍,y轴旋转90度并居中
已经像模像样了起来
引入颜色维度
散点图除了横纵坐标之外,点的颜色也可以表示一个维度的数据,这里我们用颜色来表示cloudCover这个属性
创建一个颜色比例尺
1 2 3 4
| const colorAccessor = d => d.cloudCover const colorScale = d3.scaleLinear() .domain(d3.extent(dataset, colorAccessor)) .range(["skyblue", "darkslategrey"])
|
然后修改一下绘制散点的fill属性
1
| .attr("fill", d => colorScale(colorAccessor(d)))
|
一张三维的图表就创建完毕了
最后玩点花的,把坐标轴的.domain移除,把.tickSize设置成跟长宽相等
1 2 3 4 5 6
| const xAxisGenerator = d3.axisBottom() .scale(xScale) .ticks(25) .tickSize(-dimensions.height) xAxis.selectAll('.domain') .remove()
|
y轴同理
在css里把tick的颜色设淡一点,改一下圆的半径和透明度,完工
[演示地址]