Public
Edited
Mar 18, 2024
Insert cell
Insert cell
{
// 设置一些关于尺寸的参数
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;");

/**
*
* 绘制坐标轴
*
*/
// 绘制横坐标轴
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.area() 创建一个面积生成器
const area = d3.area()
// 💡 调用面积生成器方法 area.defined() 设置数据完整性检验函数
// 该函数会在调用面积生成器时,为数组中的每一个元素都执行一次,返回布尔值,以判断该元素的数据是否完整
// 该函数传入三个入参,当前的元素 `d`,该元素在数组中的索引 `i`,整个数组 `data`
// 当函数返回 true 时,面积生成器就会执行下一步(调用坐标读取函数),最后生成该元素相应的坐标数据
// 当函数返回 false 时,该元素就会就会跳过,当前面积就会截止,并在下一个有定义的元素再开始绘制,反映在图上就是一个个分离的面积区块
// 具体可以参考官方文档 https://d3js.org/d3-shape/area#area_defined
// 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-shape#面积生成器-areas
// 这里通过判断数据点的属性 d.close(收盘价)是否为 NaN 来判定该数据是否缺失
.defined(d => !isNaN(d.close))
// 设置设置下边界线横坐标读取函数
.x(d => x(d.date))
// 设置下边界线的纵坐标的读取函数
.y0(y(0))
// 设置上边界线的纵坐标的读取函数
.y1(d => y(d.close));

// 💡 先绘制灰色的区域
svg.append("path")
.attr("fill", "#ccc")
.attr("d", area(aaplMissing.filter(d => !isNaN(d.close))));
// 其实以上的操作绘制了一个完整的面积图,由于过滤掉缺失的数据点,所以可以绘制出了一个完整(无缺口)的面积图
// 由于 **线性插值法 linear interpolation** 是面积生成器在绘制边界线时所采用的默认方法,所以对于那些缺失数据的位置,通过连接左右存在的完整点,绘制出的「模拟」线段来填补边界线的缺口

// 再绘制蓝色的面积区块(完整性的数据点)
svg.append("path")
.attr("fill", "steelblue")
.attr("d", area(aaplMissing));
// 由于含有缺失数据,所以绘制出含有缺口的面积图
// 蓝色面积图覆盖(重叠)在前面所绘制的灰色面积图上,所以最终的效果是在缺口位置由灰色的区块填补
return svg.node();
}
Insert cell
aapl = FileAttachment("aapl.csv").csv({typed: true})
Insert cell
// 💡 遍历 aapl 数组的每一个元素,修改数据点(对象)的属性 close 的值,以手动模拟数据缺失的情况
// 当数据点所对应的日期的月份小于三月份(包含),则收盘价改为 NaN;否则就采用原始值
// 📢 由于 JS 的日期中,月份是按 0 开始算起的,所以 1、2、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