散点图
散点图是一种以坐标点形式展示两个变量之间关系的可视化方式。每个数据点在图表上有两个坐标值:X 轴表示一个变量,Y 轴表示另一个变量。散点图的核心价值在于它能够直观地揭示变量之间的相关性——正相关、负相关或无相关。
与折线图不同,散点图的数据点之间不需要有线段连接,因为每个点代表的是独立的观测值。这种图表特别适合探索性数据分析,能帮助我们发现数据中的模式、异常和趋势。
什么是散点图?
散点图特别适合以下场景:探索两个数值变量之间的相关关系;识别数据中的异常值或离群点;展示分组或分类数据的分布情况;结合数据点大小表示第三个维度的信息。
但散点图也有其局限性:当数据点数量非常大时(成千上万),图表会变得非常密集难以阅读;不太适合展示时间序列数据;两个变量之外的第三维信息只能通过点的大小或颜色来间接展示。
完整散点图示例
让我们创建一个展示学生学习时间与考试成绩关系的散点图。在这个示例中:X 轴表示学习时长(小时);Y 轴表示考试成绩(分);点的颜色表示学生的专业;点的大小表示学生的出席率。
// 生成模拟数据:50 个学生的学习数据
const data = Array.from({length: 50}, (_, i) => ({
id: i + 1,
studyHours: Math.random() * 10 + 1, // 1-11 小时
score: Math.random() * 40 + 60, // 60-100 分
major: ['计算机', '数学', '物理', '化学'][Math.floor(Math.random() * 4)],
attendance: Math.random() * 20 + 80 // 80-100%
}));
// 尺寸设置
const margin = {top: 40, right: 120, bottom: 50, left: 60};
const width = 700 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
// 创建 SVG
const svg = d3.select('#chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// 比例尺设置
// X 轴:学习时长(线性比例尺)
const xScale = d3.scaleLinear()
.domain([0, 12])
.range([0, width])
.nice();
// Y 轴:考试成绩
const yScale = d3.scaleLinear()
.domain([50, 100])
.range([height, 0])
.nice();
// 颜色比例尺:按专业分类
const colorScale = d3.scaleOrdinal()
.domain(['计算机', '数学', '物理', '化学'])
.range(['#3498db', '#e74c3c', '#2ecc71', '#f39c12']);
// 大小比例尺:出席率映射为点的半径
const sizeScale = d3.scaleSqrt()
.domain([80, 100])
.range([4, 12]); // 半径范围 4-12 像素
// 添加网格线(增强可读性)
const makeXGrid = d3.axisBottom(xScale).ticks(10).tickSize(-height).tickFormat('');
const makeYGrid = d3.axisLeft(yScale).ticks(10).tickSize(-width).tickFormat('');
svg.append('g')
.attr('class', 'grid')
.attr('transform', `translate(0,${height})`)
.call(makeXGrid)
.selectAll('line')
.attr('stroke', '#e0e0e0')
.attr('stroke-opacity', 0.7);
svg.append('g')
.attr('class', 'grid')
.call(makeYGrid)
.selectAll('line')
.attr('stroke', '#e0e0e0')
.attr('stroke-opacity', 0.7);
// 移除 grid 默认的轴线
svg.selectAll('.grid .domain').remove();
// 绘制数据点
svg.selectAll('.dot')
.data(data)
.join('circle')
.attr('class', 'dot')
.attr('cx', d => xScale(d.studyHours))
.attr('cy', d => yScale(d.score))
.attr('r', d => sizeScale(d.attendance))
.attr('fill', d => colorScale(d.major))
.attr('fill-opacity', 0.7)
.attr('stroke', 'white')
.attr('stroke-width', 1);
// 添加 X 轴
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(xScale))
.append('text')
.attr('x', width / 2)
.attr('y', 40)
.attr('fill', '#333')
.attr('text-anchor', 'middle')
.text('学习时长(小时)');
// 添加 Y 轴
svg.append('g')
.call(d3.axisLeft(yScale))
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', -45)
.attr('x', -height / 2)
.attr('fill', '#333')
.attr('text-anchor', 'middle')
.text('考试成绩(分)');
// 添加图例
const legend = svg.selectAll('.legend')
.data(colorScale.domain())
.join('g')
.attr('class', 'legend')
.attr('transform', (d, i) => `translate(${width + 20},${i * 25})`);
legend.append('rect')
.attr('width', 15)
.attr('height', 15)
.attr('fill', d => colorScale(d));
legend.append('text')
.attr('x', 20)
.attr('y', 12)
.text(d => d);
这段代码展示了散点图的完整构建过程。关键点包括:使用 linear 比例尺同时处理 X 轴和 Y 轴;使用 ordinal 比例尺映射分类数据到颜色;使用 sqrt 比例尺映射第三维数据(出席率)到点的大小;sqrt 比例尺优于 linear 的地方在于它能保证面积与数值成比例,这在视觉上更直观。
关于面积与半径的数学
在散点图中,很多初学者会误用线性比例尺来设置点的半径。假设数据值从 1 变到 10,如果直接用线性比例尺映射半径(变成 1px 到 10px),实际上面积变了 100 倍(从 1π 到 100π),而不仅仅是 10 倍。为了保证视觉上的准确性,应该使用面积比例尺。D3 提供的 scaleSqrt 就是基于这个设计的:数值的平方根与面积成正比。
散点图交互功能
散点图的常见交互包括悬停提示、框选筛选、点击聚焦等。
悬停提示框
// 创建提示框(初始隐藏)
const tooltip = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('position', 'absolute')
.style('display', 'none')
.style('background', 'rgba(0,0,0,0.8)')
.style('color', '#fff')
.style('padding', '10px')
.style('border-radius', '5px')
.style('font-size', '12px')
.style('pointer-events', 'none')
.style('z-index', 1000);
// 添加交互
svg.selectAll('.dot')
.on('mouseover', function(event, d) {
// 高亮当前数据点
d3.select(this)
.attr('stroke', '#333')
.attr('stroke-width', 3)
.attr('fill-opacity', 1);
// 显示提示框
tooltip
.style('display', 'block')
.html(`
<strong>学号:</strong>${d.id}<br/>
<strong>专业:</strong>${d.major}<br/>
<strong>学习时长:</strong>${d.studyHours.toFixed(1)} 小时<br/>
<strong>考试成绩:</strong>${d.score.toFixed(1)} 分<br/>
<strong>出席率:</strong>${d.attendance.toFixed(1)}%
`);
})
.on('mousemove', function(event) {
tooltip
.style('left', (event.pageX + 15) + 'px')
.style('top', (event.pageY - 10) + 'px');
})
.on('mouseout', function(event, d) {
// 恢复样式
d3.select(this)
.attr('stroke', 'white')
.attr('stroke-width', 1)
.attr('fill-opacity', 0.7);
// 隐藏提示框
tooltip.style('display', 'none');
});
渐进式加载动画
当数据点数量较多时,「刷」一下全部显示会显得突兀。渐进式加载可以显著提升用户体验:
// 先隐藏所有点
svg.selectAll('.dot')
.attr('opacity', 0);
// 渐进式显示
svg.selectAll('.dot')
.transition()
.delay((d, i) => i * 20) // 每个点延迟 20ms
.duration(500)
.attr('opacity', 1);
气泡图
气泡图是散点图的一个变体,它利用点的大小来表示第三个维度的信息。当数据中有三个数值变量时,气泡图是非常好的选择。
与前面的示例相比,气泡图的关键在于数据驱动的半径:
// 气泡图与普通散点图的区别在于 r 属性也是数据驱动的
svg.selectAll('.dot')
.data(data)
.join('circle')
.attr('class', 'dot')
.attr('cx', d => xScale(d.xValue))
.attr('cy', d => yScale(d.yValue))
.attr('r', d => sizeScale(d.sizeValue)) // 半径来自数据
.attr('fill', d => colorScale(d.category))
// ...
此外,气泡图还有一个常用的变体是「气球图」,即用气泡内部的文字来表示数值,这样即使是小的气泡也能传达具体数值。
矩阵散点图
当需要对多个变量两两比较时,矩阵散点图(Scatter Matrix)是很有用的工具。它以网格的形式展示多个变量两两之间的散点图。
// 假设数据结构包含多个数值属性
const multiVarData = [
{x: 10, y: 20, z: 30, category: 'A'},
{x: 15, y: 25, z: 35, category: 'B'},
// ...
];
const variables = ['x', 'y', 'z'];
const cellSize = 150;
const padding = 20;
// 为每对变量创建一个子图
variables.forEach((varX, i) => {
variables.forEach((varY, j) => {
const cell = svg.append('g')
.attr('transform', `translate(${i * (cellSize + padding)},${j * (cellSize + padding)})`);
// 在每个格子中创建散点图
// 省略具体的比例尺和点绘制代码...
});
});
矩阵散点图在探索性分析中非常有用,它允许我们一眼看到所有变量两两之间的相关关系。但当变量数量超过 5-6 个时,矩阵会变得非常大,此时需要考虑其他方法(如相关系数热力图)。
总结
散点图是探索两变量关系的最佳可视化工具。D3 创建散点图的核心是使用 linear 比例尺分别处理 X 和 Y 坐标,并根据需要添加颜色和大小映射来表示更多维度的信息。交互功能对于散点图特别重要,因为数据点密集时需要通过悬停来查看具体数值。
掌握散点图后,你会发现很多可视化技巧是通用的:比例尺创建、坐标轴添加、交互事件绑定、动画效果等。这些技能可以无缝迁移到其他类型的图表制作中。