chart = {
const width = 928;
const height = 500;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 30;
const marginLeft = 40;
const x = d3.scaleUtc()
.domain(d3.extent(data, d => d.date))
.rangeRound([marginLeft, width - marginRight]);
const y = d3.scaleLinear()
// 具体参考官方文档 https://github.com/d3/d3-scale#continuous_nice
.domain(d3.extent(data, d => d.temperature)).nice()
// 设置值域范围(所映射的可视元素)
// svg 元素的高度(减去留白区域)
.rangeRound([height - marginBottom, marginTop]);
// 设置颜色比例尺
// 为不同天气状况设置不同的配色
// 使用 d3.scaleOrdinal() 排序比例尺 Ordinal Scales 将离散型的定义域映射到离散型值域
// 具体参考官方文档 https://d3js.org/d3-scale/ordinal 或 https://github.com/d3/d3-scale/tree/main#scaleOrdinal
// 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-scale#排序比例尺-ordinal-scales
const color = d3.scaleOrdinal(
// 设置定义域范围
// 各种天气状况的名称(英文缩写)
// 从 conditions.keys() 返回一个可迭代对象(包含映射的所有键名)读取共 6 种天气状况
conditions.keys(),
// 设置值域范围
// 从 conditions.values() 返回一个可迭代对象(包含映射的所有值),构成为一个数组 Array.from()
// 再从每个元素(一个对象)中读取出自定义的颜色值 d.color
Array.from(conditions.values(), d => d.color))
// 设置默认值,对于未手动设置颜色映射值的天气状况,采用黑色作为默认的映射颜色
.unknown("black");
/**
*
* 创建 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")
// 通过设置 CSS 的 transform 属性将横坐标轴容器「移动」到底部
.attr("transform", `translate(0,${height - marginBottom})`)
// 横轴是一个刻度值朝下的坐标轴
// 通过 axis.ticks(count) 设置刻度数量的参考值(避免刻度过多导致刻度值重叠而影响图表的可读性)
// 而且将坐标轴的外侧刻度 tickSizeOuter 长度设置为 0(即取消坐标轴首尾两端的刻度)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0))
// 删掉上一步所生成的坐标轴的轴线(它含有 domain 类名)
.call(g => g.select(".domain").remove());
// 💡 注意以上通过方法 selection.call(axis) 的方式来调用坐标轴对象(方法)
// 会将选择集中的元素 <g> 传递给坐标轴对象的方法,作为第一个参数
// 以便将坐标轴在相应容器内部渲染出来
// 具体参考官方文档 https://d3js.org/d3-selection/control-flow#selection_call 或 https://github.com/d3/d3-selection#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)`)
// 纵轴是一个刻度值朝左的坐标轴
.call(d3.axisLeft(y))
// 删掉上一步所生成的坐标轴的轴线(它含有 domain 类名)
.call(g => g.select(".domain").remove())
// 💡 为纵坐标轴添加注释(温度的单位标记)
// 这里并没有添加一个 <text> 元素
// 而是直接选取坐标轴的最后一个刻度(通过 class 选择器 .tick:last-of-type)里面的 `<text>` 标签
// 再在 `<text>` 元素里添加 `<tspan>` 元素,并设置内容,这样就可以为 `<text>` 元素添加额外的文本
.call(g => g.select(".tick:last-of-type text").append("tspan").text("℃"));
/**
*
* 绘制网格参考线
*
*/
svg.append("g")
.attr("stroke", "currentColor") // 设置网格参考线的颜色
.attr("stroke-opacity", 0.1) // 调小网格线的透明度
// 绘制纵向的网格参考线
.call(g => g.append("g")
// 使用 <line> 元素来绘制参考线
.selectAll("line")
// 绑定的数据是时间比例尺 x 所对应的刻度数据
// 调用时间比例尺的方法 x.ticks() 会返回其定义域的数据(一个数组,被用作刻度值)
// 返回结果可以查看 👇 下一个 📝 cell
.data(x.ticks())
.join("line") // 将线段绘制到页面上
// 分别设置各个线段的起始点的坐标 (x1, y1) 和终止点的坐标 (x2, y2),以绘制一条条纵向参考线(垂直于横坐标轴)
// 起始点的横坐标 x1 由其所绑定的数据(日期)所决定
// 通过时间比例尺 x(d) 通过日期映射/计算出相应的横坐标轴的值,并在该基础上加上 0.5px
// ❓ 由于刻度线的宽度默认是 1px,所以进行 0.5px 偏移,让网格参考线位于刻度线的中间开始延伸
.attr("x1", d => 0.5 + x(d))
//(纵向参考线)终止点的横坐标 x2 和 x1 的值是一样的
.attr("x2", d => 0.5 + x(d))
// 起始点的纵坐标 y1 等于 marginTop(相当于位于 svg 顶部,但减去了顶部的留白)
.attr("y1", marginTop)
// 终止点的纵坐标 y2 等于 height - marginBottom(相当于位于 svg 底,但减去了底部的留白)
.attr("y2", height - marginBottom))
// 绘制横向的网格参考线
.call(g => g.append("g")
.selectAll("line")
// 绑定的数据是线性比例尺 y 所对应的刻度数据
// 返回结果可以查看 👇 以下第二个 📝 cell
.data(y.ticks())
.join("line")
.attr("y1", d => 0.5 + y(d))
.attr("y2", d => 0.5 + y(d))
.attr("x1", marginLeft)
.attr("x2", width - marginRight));
/**
*
* 创建线性渐变色
*
*/
// 使用 Observable 平台的标准库所提供的方法 DOM.uid(namespace) 创建一个唯一 ID 号
// 具体参考 https://observablehq.com/@observablehq/stdlib#cell-790
// 用作元素 <linearGradient> 的 id 属性值
const colorId = DOM.uid("color");
// 使用 svg 元素 <linearGradient> 定义线性渐变色,用于图形元素的填充或描边
svg.append("linearGradient")
// 设置 id 属性
// 使用该值来引用/指向该渐变色,以将其应用到图形元素上
.attr("id", colorId.id)
// 💡 设置 gradientUnits 属性,它用于配置渐变的坐标系(涉及 x1, x2 等属性)
// 它的属性值可以设置为 `userSpaceOnUse` 或 `objectBoundingBox`(默认值)
// 这两个值的区别在于坐标的**参考系**不同
// * 属性值 userSpaceOnUse 表示渐变中的坐标值是相对于用户坐标系统的,即无论渐变被应用到哪个元素上,它的坐标都是相对于**整个 SVG 画布的 viewport 视图**
// * 属性值 objectBoundingBox(默认值)表示渐变中的坐标值是相对于**引用元素的边界框**的,即渐变的坐标将根据引用元素的大小和位置进行缩放和定位,其中 (0,0) 表示边界框的左上角,(1,1) 表示边界框的右下角
// 这里采用 userSpaceOnUse 即以整个 SVG 视图作为渐变坐标的参考系
// 由于使用 <stop> 元素进行渐变色的切换时,其定位(属性 offset)是通过横坐标值 x(d.date) 和 svg 的宽度计算得到的(比例)
// 而横坐标轴比例尺 x 和 svg 的宽度,这两者的坐标的参考系都是相对于整个 SVG 视图的
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", 0) // 渐变色的起始点的横坐标
.attr("x2", width) // 渐变色的终止点的横坐标
// 这里没有设置起始点和终止点的纵坐标,由于渐变色是沿横坐标轴变化的,而且它是应用到线段的描边上(并不是填充)
// 所以纵坐标采用默认值 0% 即可
// 进行二次选择,在元素 <linearGradient> 内添加一系列的 <stop> 元素,以切换渐变色
.selectAll("stop")
.data(data) // 绑定数据
.join("stop") // 将 <stop> 元素添加到页面上
// 通过 offset 属性设置该颜色的定位(相对于整个 SVG 视图),采用 x(d.date) / width 百分比形式
// 所绑定的数据(对象)的属性 d.date 获取日期,再通过比例尺 x 的映射 x(d.date) 得到对应的横坐标
// 再与 svg 的宽度相比 x(d.date) / width 得到百分比
.attr("offset", d => x(d.date) / width)
// 通过 stop-color 属性设置颜色
// 所绑定的数据(对象)的属性 d.condition 获取天气状况,再通过比例尺 color 的映射 color(d.condition) 得到对应的颜色值
.attr("stop-color", d => color(d.condition));
/**
*
* 绘制折线图内的线段
*
*/
// 使用方法 d3.line() 创建一个线段生成器
// 线段生成器会基于给定的坐标点生成线段(或曲线)
// 具体可以参考官方文档 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
const line = d3.line()
// 设置两点之间的曲线插值器,这里使用 D3 所提供的一种内置曲线插值器 d3.curveStep
// 该插值效果是在两个数据点之间,生成阶梯形状的线段
// 具体效果参考 https://d3js.org/d3-shape/curve#curveStep 或 https://github.com/d3/d3-shape#curveStep
.curve(d3.curveStep)
// 设置横坐标读取函数
// 该函数会在调用线段生成器时,为数组中的每一个元素都执行一次,以返回该数据所对应的横坐标
// 这里基于每个数据点的日期 d.date 并采用比例尺 x 进行映射,计算出相应的横坐标
.x(d => x(d.date))
// 设置纵坐标读取函数
.y(d => y(d.temperature));
// 将线段路径绘制到页面上
svg.append("path") // 使用路径 <path> 元素绘制折线
// 绑定数据
// 这里采用 selection.datum(value) 为选择集中的每个元素上绑定的数据(该选择集里只有一个 <path> 元素)
// ⚠️ 它与 selection.data(data) 不同,该方法不会将数组进行「拆解」,即这个方法不会进行数据链接计算并且不影响索引,不影响(不产生)enter 和 exit 选择集,而是将数据 value 作为一个整体绑定到选择的各个元素上,因此使用该方法选择集的所有 DOM 元素绑定的数据都一样
// 具体参考官方文档 https://d3js.org/d3-selection/joining#selection_datum 或 https://github.com/d3/d3-selection/tree/main#selection_datum
// 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/module-api/d3-module-selection#绑定数据
.datum(data)
// 只需要路径的描边作为折线,不需要填充,所以属性 fill 设置为 none
.attr("fill", "none")
// 设置描边颜色
// 这里通过 id 值来引用/使用前面所创建的渐变色
.attr("stroke", colorId)
// 设置描边宽度
.attr("stroke-width", 2)
// 设置折线之间的连接样式(圆角让连接更加平滑)
.attr("stroke-linejoin", "round")
// 设置路径端点的样式
.attr("stroke-linecap", "round")
// 调用线段生成器 line
// 将其返回的结果(字符串)作为 `<path>` 元素的属性 `d` 的值
.attr("d", line);
return Object.assign(svg.node(), {scales: {color}});
}