Public
Edited
Mar 18, 2024
Insert cell
Insert cell
chart = {
// 设置一些关于尺寸的参数
const width = 928;
const height = 500;
const marginTop = 20;
const marginRight = 30;
const marginBottom = 30;
const marginLeft = 40;
/**
*
* 构建比例尺
*
*/
// 设置横坐标轴的比例尺
const x = d3.scaleUtc(d3.extent(aapl, d => d.date), [marginLeft, width - marginRight]);

// 设置纵坐标轴的比例尺
const y = d3.scaleLinear([0, d3.max(aapl, d => d.close)], [height - marginBottom, marginTop]);

/**
*
* 创建 svg 容器
*
*/
// 返回的是一个包含 svg 元素的选择集
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");

/**
*
* 绘制坐标轴
*
*/
// 绘制横坐标轴
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0));

// 绘制纵坐标轴,并添加网格参考线和坐标轴注释信息
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y).ticks(height / 40))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("↑ Daily close ($)"));

/**
*
* 绘制折线图内的线段
*
*/
// 使用方法 d3.line() 创建一个线段生成器
const line = d3.line()
// 💡 调用线段生成器方法 line.defined() 设置数据完整性检验函数
// 该函数会在调用线段生成器时,为数组中的每一个元素都执行一次,返回布尔值,以判断该元素的数据是否完整
// 该函数也是有三个入参,当前的元素 `d`,该元素在数组中的索引 `i`,整个数组 `data`
// 当函数返回 true 时,线段生成器就会执行下一步(调用坐标读取函数),最后生成该元素相应的坐标数据
// 当函数返回 false 时,该元素就会就会跳过,当前线段就会截止,并在下一个有定义的元素再开始绘制,反映在图上就是一段段分离的线段
// 具体可以参考官方文档 https://d3js.org/d3-shape/line#line_defined
// 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-shape#线段生成器-lines
// 这里通过判断数据点的属性 d.close(收盘价)是否为 NaN 来判定该数据是否缺失
.defined(d => !isNaN(d.close))
// 设置横坐标读取函数
.x(d => x(d.date))
.y(d => y(d.close));
// 💡 先绘制灰色的线段
svg.append("path")
.attr("fill", "none")
.attr("stroke", "#ccc")
.attr("stroke-width", 1.5)
.attr("d", line(aaplMissing.filter(d => !isNaN(d.close))));
// 其实以上的操作绘制了一个完整的折线图,由于过滤掉缺失的数据点,所以可以绘制出了一个完整(无缺口)的折线图
// 由于 **线性插值法 linear interpolation** 是线段生成器在绘制线段时所采用的默认方法,所以对于那些缺失数据的位置,通过连接左右存在的完整点,绘制出的「模拟」线段来填补缺口

// 再绘制蓝色的线段(完整性的数据点)
svg.append("path")
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", line(aaplMissing));
// 由于含有缺失数据,所以绘制出含有缺口的折线图
// 蓝色折线图覆盖(重叠)在前面所绘制的灰色折线图上,所以最终的效果是在缺口位置由灰色的线段填补

return svg.node();
}
Insert cell
aapl = FileAttachment("aapl.csv").csv({typed: true})
Insert cell
// 💡 遍历 aapl 数组的每一个元素,修改数据点(对象)的属性 close 的值,以手动模拟数据缺失的情况
// 当数据点所对应的日期的月份小于三月份(包含),则收盘价改为 NaN;否则就采用原始值
// 📢 由于 JS 的日期中,月份是按 0 开始算起的,所以 1、2、3 月份是满足以下的判断条件 d.date.getUTCMonth() < 3
aaplMissing = aapl.map(d => ({...d, close: d.date.getUTCMonth() < 3 ? NaN : d.close})) // simulate gaps
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