Public
Edited
Jul 30, 2024
Insert cell
Insert cell
viewof selection = {

// 使用方法 d3.create("svg") 创建一个 svg 元素,并返回一个选择集 selection
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]) // viewBox 一般设置为与 svg 元素等宽高
.property("value", []); // 为 svg 添加一个属性 value 为了后面刷选时可以记录哪些点被选中

// 创建一个刷选器
const brush = d3.brush()
.on("start brush end", brushed); // 监听刷选的全过程(刷选在不同过程会分发三个不同类型的事件),触发回调函数 brushed

// 绘制横坐标
svg.append("g")
.call(xAxis);

// 绘制纵坐标
svg.append("g")
.call(yAxis);

// 绘制数据点
const dot = svg.append("g") // 在 svg 中添加一个容器 <g> 元素,并在其上设置一些数据点的通用样式
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.selectAll("circle") // 将数据集 data 和一系列「虚拟」的占位 <circle> 元素进行绑定
.data(data)
.join("circle") // 将这些 <circle> 生成到 <g> 容器中
.attr("transform", d => `translate(${x(d.x)},${y(d.y)})`) // 通过 CSS 的偏移 translate 样式来将各个 <circle> 元素移动到相应的位置
.attr("r", 3); // 设置圆的半径大小

svg.call(brush); // 将前面所创建的刷选器绑定到 svg 上
// 刷选器会创建一系列 SVG 元素以展示选区,并响应用户的刷选操作。

// 刷选发生时所触发的回调函数
// 从入参的刷选事件对象中解构出 selection 选区属性
function brushed({selection}) {
let value = [];
// 如果用户创建了选区
if (selection) {
const [[x0, y0], [x1, y1]] = selection; // 将选区解构出各个坐标值
value = dot
.style("stroke", "gray") // 先将所有的数据点设置为灰色
.filter(d => x0 <= x(d.x) && x(d.x) < x1 && y0 <= y(d.y) && y(d.y) < y1) // 筛选出所有数据点中满足条件的元素(构成新的选择集),即在 [[x0, y0], [x1, y1]] 范围内的数据点
.style("stroke", "steelblue") // 将选区范围内的数据点设置为蓝色
.data(); // 返回选择集中的元素所绑定的数据所构成的一个数组
} else {
// 如果用户取消了选区
dot.style("stroke", "steelblue"); // 将所有的数据点恢复为蓝色
}
svg.property("value", value).dispatch("input"); // 设置 svg 的属性 value 值(如果选区为空,则该值为 [] 空数组;如果创建了选区,则该值为选中的数据点所绑定的数据所构成的一个数组),并分发一个 `input` 事件(Observable 会监听,并响应式地改变下一个代码块的值)
}

return svg.node();
}
Insert cell
selection
Insert cell
height = 600 // 散点图的高
Insert cell
margin = ({top: 20, right: 30, bottom: 30, left: 40}) // 为散点图设置一些「安全」边距
Insert cell
// 横坐标轴所使用的比例尺类型
// 使用 d3-scale 模块 https://github.com/d3/d3-scale 的方法 `d3.scaleLinear()` 创建线性比例尺
x = d3.scaleLinear()
// 设置定义域
// 使用 d3-array 模块 https://github.com/d3/d3-array 的方法 `d3.extent()` 获取 data 各数据点的 x 的范围,作为比例尺的定义域
// 还使用 d3-scale 模块的连续型比例尺的方法 `continuous.nice()` 通过四舍五入使定义域的两端的值更「整齐」nice
.domain(d3.extent(data, d => d.x)).nice()
// 设置值域
.range([margin.left, width - margin.right]) // 这里的 width 参数是使用了 Observable 标准库 https://observablehq.com/@observablehq/stdlib#widthSection 所提供的功能,其值会随着网页页面大小的改变而响应式地更改
Insert cell
// 纵坐标轴所使用的比例尺类型
y = d3.scaleLinear()
.domain(d3.extent(data, d => d.y)).nice()
.range([height - margin.bottom, margin.top])
Insert cell
// 基于比例尺绘制横坐标轴
// 这是一个函数,接收一个 D3 选择集作为参数,其中一般只有一个 <g> 元素,以其作为一个容器(包含坐标轴的轴线和坐标刻度以及坐标值等元素),最后依然返回该选择集
// 通过一系列的链式调用,主要是使用方法 selection.attr() 为选择集中的元素(当前选择集包含的元素是 <g> 元素)设置属性
xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`) // 将横坐标轴容器定位到底部
// 以下使用了一系列的 selection.call() 方法,关于其具体作用和用法可以参考 https://github.com/d3/d3-selection#selection_call
.call(d3.axisBottom(x)) // 使用方法 d3.axisBottom(scale) 生成一个朝下的坐标轴(对象),即其刻度在水平轴线的下方
.call(g => g.select(".domain").remove()) // 删掉上一步所生成的坐标轴的轴线(它含有 domain 类名)
.call(g => g.append("text") // 为坐标轴添加额外信息名称(一般是刻度值的单位等信息)
.attr("x", width - margin.right)
.attr("y", -4)
.attr("fill", "#000") // 设置文本样式
.attr("font-weight", "bold")
.attr("text-anchor", "end")
.text(data.x)) // 设置文本内容
Insert cell
// 基于比例尺绘制纵坐标轴
yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
.call(g => g.select(".domain").remove())
// 为坐标轴添加额外信息名称(一般是刻度值的单位等信息)
// 这里直接复制了坐标轴的最后一个坐标刻度值,然后在水平方向上做了一些偏移,再修改文本内容
// 其实有另一个实现方案,就是和横坐标轴类似,添加一个 <text> 元素,不过除了设置水平方向的偏移,还有设置一点垂直方向的偏移,例如 .attr("y", 10)
.call(g => g.select(".tick:last-of-type text").clone()
.attr("x", 4)
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text(data.y))
Insert cell
// read the csv file and import the data as an array (elements are objects)
// refer to d3-dsv module: https://github.com/d3/d3-dsv
// 以上使用 d3-dsv 模块 https://github.com/d3/d3-dsv 的方法 `d3.csvParse()` 来解析 csv 文件,构建出一个对象数组
// 其中将各数据点的 Miles_per_Gallon 属性重命名为 x(在散点图中作为横坐标轴的值),Horsepower 属性重命名为 y(在散点图中作为纵坐标轴的值)
// 然后通过 `Object.assign()` 方法来为该数组添加两个属性 `x` 和 `y` 作为横坐标轴和纵坐标轴的名称,当然原来该数组也有 `columns` 属性
data = Object.assign(d3.csvParse(await FileAttachment("cars-2.csv").text(), ({Name: name, Miles_per_Gallon: x, Horsepower: y}) => ({name, x: +x, y: +y})), {x: "Miles per Gallon", y: "Horsepower"})
//以下列出数组的一些「额外属性」,之后会用到
Insert cell
data.x
Insert cell
data.y
Insert cell
data.columns
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more