chart = {
const width = 928;
const height = 600;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 30;
const marginLeft = 30;
const x = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([marginLeft, width - marginRight]);
const y = d3.scaleLinear()
// 相应地 ymax 就是最高温度
.domain([
d3.min(data, d => Math.min(d.value0, d.value1)),
d3.max(data, d => Math.max(d.value0, d.value1))
])
// 设置值域范围
// svg 元素的高度(减去留白区域)
.range([height - marginBottom, marginTop]);
// 设置颜色比例尺
// 为不同的面积区域设置不同的颜色,以对应数据的不同(差异)关系
// 只有两种情况:其中一种情况是当天温度纽约高于三藩市;另一种情况是当前温度三藩市高于纽约
// 所以只需要提供两种颜色进行映射对照
// 这里使用 D3 内置的一种配色方案 d3.schemeRdYlBu(它属于 Diverging schemes 离散型的配色方案,用于明显地区分不同的类型)
// 它是一个嵌套数组,包含一些预设的配色方案(共 9 种对色谱采样的方式)
// 具体可以查看 👇 下面第一个 📝 cell 或参考官方文档 https://d3js.org/d3-scale-chromatic/diverging#schemeRdYlBu
// 这里采用第三个配色方案 d3.schemeRdYlBu[3] 它也是一个数组,包含 3 个元素,每个元素都是一个表示颜色的字符串
// 具体可以查看 👇👇 下面第二个 📝 cell
// d3.schemeRdYlBu[3][2] 获取一种颜色(#91bfdb 浅蓝色),d3.schemeRdYlBu[3][0] 获取另一种颜色(#fc8d59 橙色),它们分别用于映射对照不同的数据(差异)关系
const colors = [d3.schemeRdYlBu[3][2], d3.schemeRdYlBu[3][0]];
/**
*
* 创建 svg 容器
*
*/
// 返回的是一个包含 svg 元素的选择集
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;")
// 绑定数据
// 这里采用 selection.datum(value) 为选择集中的每个元素上绑定的数据(该选择集里只有一个 <svg> 元素)
// ⚠️ 它与 selection.data(value) 不同,该方法不会将数组进行「拆解」
// 即这个方法不会进行数据与元素的一一链接计算,并且不影响索引,不影响(不产生)enter 和 exit 选择集
// 而是将数据 value 作为一个整体绑定到选择的各个元素上,因此使用该方法选择集的所有 DOM 元素绑定的数据都一样
// 具体参考官方文档 https://d3js.org/d3-selection/joining#selection_datum
// 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/module-api/d3-module-selection#绑定数据
.datum(data);
/**
*
* 绘制坐标轴
*
*/
// 绘制横坐标轴
svg.append("g")
// 通过设置 CSS 的 transform 属性将横坐标轴容器「移动」到底部
.attr("transform", `translate(0,${height - marginBottom})`)
// 横轴是一个刻度值朝下的坐标轴
.call(d3.axisBottom(x)
// 通过 axis.ticks(count) 设置刻度数量的参考值(避免刻度过多导致刻度值重叠而影响图表的可读性)
.ticks(width / 80)
// 而且将坐标轴的外侧刻度 tickSizeOuter 长度设置为 0(即取消坐标轴首尾两端的刻度)
.tickSizeOuter(0))
// 删掉上一步所生成的坐标轴的轴线(它含有 domain 类名)
.call(g => g.select(".domain").remove());
// 💡 注意以上使用的是方法 selection.call(axis) 的方式来调用坐标轴对象(方法)
// 会将选择集中的元素 <g> 传递给坐标轴对象的方法,作为第一个参数
// 以便将坐标轴在相应容器内部渲染出来
// 具体参考官方文档 https://d3js.org/d3-selection/control-flow#selection_call
// 或这一篇文档 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-data-binding#其他方法
// 绘制纵坐标轴
svg.append("g")
// 通过设置 CSS 的 transform 属性将纵向坐标轴容器「移动」到左侧
.attr("transform", `translate(${marginLeft},0)`)
// 纵轴是一个刻度值朝左的坐标轴
// 通过 axis.ticks(count) 设置刻度数量的参考值(避免刻度过多导致刻度值重叠而影响图表的可读性)
.call(d3.axisLeft(y))
// 删掉上一步所生成的坐标轴的轴线(它含有 domain 类名)
.call(g => g.select(".domain").remove())
// 复制了一份刻度线,用以绘制图中横向的网格参考线
.call(g => g.selectAll(".tick line").clone()
// 调整复制后的刻度线的终点位置(往右移动)
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1)) // 调小参考线的透明度
// 为纵坐标轴添加标注信息
// 并选中最后一个刻度值,即 <text> 元素,进行复制
.call(g => g.select(".tick:last-of-type text").clone()
.attr("x", -marginLeft) // 设置元素的偏移量
.attr("y", -30)
.attr("fill", "currentColor") // 设置文字的颜色
.attr("text-anchor", "start") // 设置文字的对齐方式
.text("↑ Temperature (°F)")); // 设置文本内容
/**
*
* 绘制面积图内的面积形状
*
*/
// 绘制表示橙色的面积图(三藩市的温度高于纽约),面积图的上边界是三藩市的温度,下边界是纽约的温度,填充色为橙色
// 而对于三藩市低于纽约的日子,则不绘制面积图(可以将这段时间的面积图的下边界也设定为三藩市的温度,则该时间段的面积图绘制为一条线,再将折线的描边设置为透明即可)
svg.append("path") // 使用路径 <path> 元素绘制面积形状
.attr("fill", colors[1]) // 设置填充颜色为橙色
// 使用方法 d3.area() 创建一个面积生成器,它会根据给定的数据(svg 所绑定的数据)设置 <path> 路径形状
.attr("d", d3.area()
// 设置两点之间的曲线插值器
.curve(d3.curveStep)
// 设置下边界线横坐标读取函数
.x(d => x(d.date))
// 设置下边界线的纵坐标的读取函数,它始终是 height(即位于 svg 的底部位置,即横坐标轴)
.y0(d => d.value0 > d.value1 ? y(d.value1) : y(d.value0))
// 设置上边界线的纵坐标的读取函数,基于 d.value0 三藩市的温度,并采用比例尺 y 进行映射,得到纵坐标轴在 svg 中的坐标位置
.y1(d => y(d.value0)));
// 绘制表示浅蓝色的面积图(三藩市的温度低纽约),面积图的上边界是纽约的温度,下边界是三藩市的温度,填充色为浅蓝色
// 而对于三藩市高于纽约的日子,则不绘制面积图(可以将这段时间的面积图的下边界也设定为纽约的温度,则该时间段的面积图绘制为一条线,再将折线的描边设置为透明即可)
svg.append("path") // 使用路径 <path> 元素绘制面积形状
.attr("fill", colors[0]) // 设置填充颜色为浅蓝色
// 使用方法 d3.area() 创建一个面积生成器,它会根据给定的数据(svg 所绑定的数据)设置 <path> 路径形状
.attr("d", d3.area()
// 设置两点之间的曲线插值器
.curve(d3.curveStep)
// 设置下边界线横坐标读取函数
.x(d => x(d.date))
// 设置下边界线的纵坐标的读取函数,它始终是 height(即位于 svg 的底部位置,即横坐标轴)
.y0(d => d.value0 > d.value1 ? y(d.value1) : y(d.value0))
// 设置上边界线的纵坐标的读取函数,基于 d.value1 纽约的温度,并采用比例尺 y 进行映射,得到纵坐标轴在 svg 中的坐标位置
.y1(d => y(d.value1)));
// 绘制一条黑色的线,表示三藩市的温度随时间的变化
svg.append("path") // 使用路径 <path> 元素绘制折线
.attr("fill", "none") // 由于折线不需要填充颜色,所以属性 fill 设置为 none
.attr("stroke", "black") // 设置折线的描边颜色为黑色
.attr("stroke-width", 1.5) // 设置描边的宽度
.attr("stroke-linejoin", "round") //
.attr("stroke-linecap", "round") // 设置折线之间的连接样式(圆角让连接更加平滑)
// 使用方法 d3.line() 创建一个线段生成器,线段生成器会基于给定的数据(svg 所绑定的数据)生成线段(或曲线)
// 调用线段生成器时返回的结果,会基于生成器是否设置了画布上下文 context 而不同。如果设置了画布上下文 context,则生成一系列在画布上绘制路径的方法,通过调用它们可以将路径绘制到画布上;如果没有设置画布上下文 context,则生成字符串,可以作为 `<path>` 元素的属性 `d` 的值
// 具体可以参考官方文档 https://d3js.org/d3-shape/line 或 https://github.com/d3/d3-shape/tree/main#lines
// 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-shape#线段生成器-lines
.attr("d", d3.line()
// 设置两点之间的曲线插值器
.curve(d3.curveStep)
// 该函数会在调用线段生成器时,为数组中的每一个元素都执行一次横坐标读取函数和纵坐标读取函数,以返回该数据所对应的横纵坐标值
// 设置横坐标读取函数
// 这里基于每个数据点的日期(时间)d.date 并采用比例尺 x 进行映射,计算出相应的横坐标
.x(d => x(d.date))
// 设置纵坐标读取函数
// 这里基于每个数据点的三藩市的温度 d.value0 并采用比例尺 y 进行映射,计算出相应的纵坐标
.y(d => y(d.value0)));
// 绘制一条绿色的线,表示纽约的温度随时间的变化
// svg.append("path")
// .attr("fill", "none")
// .attr("stroke", "green")
// .attr("stroke-width", 1.5)
// .attr("stroke-linejoin", "round")
// .attr("stroke-linecap", "round")
// .attr("d", d3.line()
// .curve(d3.curveStep)
// .x(d => x(d.date))
// .y(d => y(d.value1)));
return svg.node();
}