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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more