diff --git a/areachart/shape-tweening/app.js b/areachart/shape-tweening/app.js
index eff24bb..6de8510 100644
--- a/areachart/shape-tweening/app.js
+++ b/areachart/shape-tweening/app.js
@@ -165,7 +165,7 @@ d3.json(dataURL).then((polygon) => {
// 而且新的过渡会**在前一个过渡结束后开始执行**
// 一般通过该方法为同一个选择集合设置一系列**依次执行的过渡动效**
.transition()
- .delay(5000) // 设置过渡的时间
+ .delay(5000) // 设置过渡的延迟/等待时间
// 这里是将面积形状从圆形切换回多边形
.attr("d", d0)
// 最后通过方法 transition.end() 返回一个 Promise,仅在过渡管理器所绑定的选择集合的所有过渡完成时才 resolve
diff --git a/areachart/streamgraph-transition/app.js b/areachart/streamgraph-transition/app.js
new file mode 100644
index 0000000..8e1e610
--- /dev/null
+++ b/areachart/streamgraph-transition/app.js
@@ -0,0 +1,213 @@
+// 参考自 https://observablehq.com/@d3/streamgraph-transitions
+
+/**
+ *
+ * 构建 svg
+ *
+ */
+const container = document.getElementById("container"); // 图像的容器
+
+// 获取尺寸大小
+const width = container.clientWidth; // 宽度
+const height = container.clientHeight; // 高度
+
+// 创建 svg
+// 在容器
元素内创建一个 SVG 元素
+// 返回一个选择集,只有 svg 一个元素
+const svg = d3
+ .select("#container")
+ .append("svg")
+ .attr("width", width)
+ .attr("height", height)
+ .attr("viewBox", [0, 0, width, height]);
+
+/**
+ *
+ * 一些与河流图相关的变量数据
+ *
+ */
+// (堆叠的)系列的数量
+const n = 20 // number of layers
+
+// 每个系列的数据点的数量
+const m = 200 // number of samples per layer
+
+// 每个堆叠层的波动的数量
+// 💡 将数据的波峰形象地称为 bump 「隆起」,这个术语强调的是数据曲线的起伏,反映出数据在某个时间段内的变化
+// 这里设置 k=10 意味着每一个堆叠层会有 10 个这样的隆起,使得河流图更具起伏感和流动性,而非平滑无变化的线条
+const k = 10 // number of bumps per layer
+
+/**
+ *
+ * 构建比例尺
+ *
+ */
+// 设置横坐标轴的比例尺
+// 横坐标轴的数据是数据点的索引值(依次对应 200 个数据点),使用 d3.scaleLinear 构建一个线性比例尺
+// 定义域范围 [0, m-1](由数据点的数量 m=200 决定),值域范围是 svg 元素的宽度
+const x = d3.scaleLinear([0, m - 1], [0, width]);
+
+// 设置纵坐标轴的比例尺
+// 纵坐标轴的数据是连续型的数值(随机生成的数据),使用 d3.scaleLinear 构建一个线性比例尺
+// 定义域范围初始值先设置为 [0, 1] 后续根据数据集(从中提取最大值和最小值)进行修改,值域范围是 svg 元素的高度
+const y = d3.scaleLinear([0, 1], [height, 0]);
+
+/**
+ *
+ * 设置配色方案
+ *
+ */
+// 使用 D3 内置的一种配色方案 d3.interpolateCool
+// 通过方法 `d3.interpolateCool(t)` 从色谱中获取一种颜色,参数 t 取值范围是 [0, 1]
+// 具体可以参考官方文档 https://d3js.org/d3-scale-chromatic/sequential#interpolateCool
+// 采用的色谱是 Niccoli’s perceptual rainbow 具体参考 https://mycartablog.com/2013/02/21/perceptual-rainbow-palette-the-method/
+// 为不同系列/堆叠层设置不同的颜色
+const z = d3.interpolateCool;
+
+/**
+ *
+ * 创建一个堆叠生成器,用于对数据进行转换
+ *
+ */
+// 决定有哪些系列进行堆叠可视化
+// 通过堆叠生成器对数据进行转换,便于后续绘制堆叠图
+// 具体可以参考官方文档 https://d3js.org/d3-shape/stack
+// 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-shape#堆叠生成器-stacks
+const stack = d3.stack()
+ // 设置系列的名称(数组)
+ // 使用 d3.range(n) 快速生成一个等差数列,并用数列各项作为元素构成一个数组返回
+ // 这里生成一个具有 n 个元素的数组,第一个元素是 0,最后一个元素是 n-1
+ // 该方法来自 d3-array 模块,具体可以参考官方文档 https://d3js.org/d3-array/ticks#range
+ // 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-data-process#刻度生成
+ // D3 为每一个系列都设置了一个属性 key,其值是系列名称(生成面积图时,系列堆叠的顺序就按照系列名称的排序)
+ .keys(d3.range(n))
+ // 设置基线函数,通过更新堆叠图的上下界的值,可以调整图形整体的定位
+ // D3 提供了一系列内置的基线函数,它们的具体效果可以参考 https://d3js.org/d3-shape/stack#stack-offsets
+ // 默认使用内置基线函数 d3.stackOffsetNone 以零为基线
+ // 这里使用另一种内置基线函数 d3.stackOffsetWiggle 通过移动基线,以最大程度地减小各系列的「振幅」(即各系列沿着横轴上下摆动的幅度),让河流图看起来更美观、流畅、易读
+ // 可以阅读相关文章 https://leebyron.com/streamgraph/ 对这种算法的介绍
+ .offset(d3.stackOffsetWiggle)
+ // 设置排序函数,即决定堆叠图中各系列的叠放次序
+ // 该函数返回的是一个数组(称为排序数组 order),里面的元素是一个表示索引的数值,依次对应于系列名称数组的元素,表示各系列的排序/叠放优先次序
+ // D3 提供了一系列内置的排序函数,它们的具体效果可以参考 https://d3js.org/d3-shape/stack#stack-orders
+ // 默认使用内置排序函数 d3.stackOrderNone 它不对排序/叠放次序进行改变
+ // 即按照系列名称数组(通过方法 stack.keys() 所设置的)来排序
+ .order(d3.stackOrderNone);
+
+// 该函数用于生成随机数,可以让数据符合特定的分布特点
+// 参考自 Lee Byron 的测试数据集生成器 https://leebyron.com/streamgraph/
+function bump(a, n) {
+ const x = 1 / (0.1 + Math.random());
+ const y = 2 * Math.random() - 0.5;
+ const z = 10 / (0.1 + Math.random());
+ // 遍历数组 a 中的每个元素,对其值进行调整
+ for (let i = 0; i < n; ++i) {
+ const w = (i / n - y) * z;
+ // 基于原始值 a[i] 再添加随机生成的数
+ a[i] += x * Math.exp(-w * w);
+ }
+}
+
+// 该函数生成用于绘制河流图(每个系列)的数据
+function bumps(n, m) {
+ const a = [];
+ // 为数组 a 各个元素(共 n 个)设置初始值
+ for (let i = 0; i < n; ++i) a[i] = 0; // 初始值均为 0
+
+ // 迭代 m 次,为数组 a 各个元素设置值(随机数)
+ // 让该数据集的波动的数量具有 m 个波峰
+ for (let i = 0; i < m; ++i) bump(a, n);
+
+ // 该函数最后返回值的是一个数组
+ return a;
+};
+
+// 该函数用于生成数据集,同时设置纵坐标轴的定义域范围
+function randomize() {
+ // 使用堆叠生成器对数据集进行处理,最后返回一个数组,每一个元素都是一个系列(整个面积图就是由多个系列堆叠而成的)
+ // 例如在本实例中,共有 20 个系列,所以返回的数组具有 20 个元素
+ // 而每一个元素(系列)也是一个数组,其中每个元素是属于该系列的一个数据点,例如在本示例中,每个系列会有 200 个数据点
+ // ⚠️ 由于堆叠生成器没有调用方法 stack.value() 设置自定义的(各系列的数据)读取函数,所以采用默认的读取函数
+ // 默认的读取函数是 `function value(d, key) { return d[key]; }`
+ // 在调用堆叠生成器对原始数据进行转换过程中,每一个原始数据 d 和系列名称 key(就是通过方法 stack.keys() 所设置的数组中的元素)会作为入参,分别调用该函数,以从原始数据中获取相应系列的数据
+ // ⚠️ 由于这里采用默认的读取函数,所以构建的数据集的结构要与之相匹配,一般需要是一个对象数组,即每个元素都是对象,其中对象包含一系列属性,属性名是系列名,属性值是该数据点中相应系列的值
+ // ⚠️ 在这里由于 key 采用索引值,所以最终只需要构建一个嵌套数组(而非对象数组)即可,子数组的索引值就相当于系列名
+ // 这里先使用 JavaScript 数组的原生方法 Array.from(arrayLike, mapFn) 生成一个数组
+ // 在这里参数 arrayLike 是一个对象,包含属性 length=20,所以生成一个包含 20 个元素的数组,每个元素初始值是 undefined
+ // 然后再遍历每个元素并执行参数 mapFn 所设置的映射函数,以修改元素的值
+ // 在这里参数 mapFn 是函数 () => bumps(m, k) 该函数返回返回值的是一个数组,包含 m=200 个元素,这些数据的分布特点是具有 k=10 个波峰(使得河流图更具起伏感和流动性,而非平滑无变化的线条)
+ // 所以使用 Array.from() 创建的是一个嵌套数组,它有 20 个元素(表示 20 个系列),每个元素也是数组(表示每个系列包含 200 个数据点)
+ // 最后再使用 d3.transpose(matrix) 对嵌套数组进行转换,也是得到一个嵌套数组,但是结构有所不同
+ // 关于该方法的具体介绍可以参考官方文档 https://d3js.org/d3-array/transform#transpose
+ // 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-data-process#转换
+ // 转换得到的嵌套数组变成了具有 200 个元素(表示 200 个原始数据点),每个元素也是数组(包含 20 个元素,分别对应于 20 个系列)
+ // 这种数据结构就可以对应堆叠生成默认的读取函数
+ const layers = stack(d3.transpose(Array.from({length: n}, () => bumps(m, k))));
+
+ // 更改纵坐标轴的定义域
+ // 以数据集中的最小值和最大值作为纵坐标轴的定义域范围
+ y.domain([
+ d3.min(layers, l => d3.min(l, d => d[0])), // 数据集中的最小值
+ d3.max(layers, l => d3.max(l, d => d[1])) // 数据集中的最大值
+ ]);
+
+ return layers;
+}
+
+/**
+ *
+ * 绘制面积图内的面积形状
+ *
+ */
+// 使用 d3.area() 创建一个面积生成器
+// 面积生成器会基于给定的数据生成面积形状
+// 调用面积生成器时返回的结果,会基于生成器是否设置了画布上下文 context 而不同。如果设置了画布上下文 context,则生成一系列在画布上绘制路径的方法,通过调用它们可以将路径绘制到画布上;如果没有设置画布上下文 context,则生成字符串,可以作为 `
` 元素的属性 `d` 的值
+// 具体可以参考官方文档 https://d3js.org/d3-shape/area
+// 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-shape#面积生成器-areas
+const area = d3.area()
+// 设置下边界线横坐标读取函数
+// 💡 不需要再设置上边界线横坐标读取函数,因为默认会复用相应的下边界线横坐标值,这符合横向延伸的面积图
+// 该函数会在调用面积生成器时,为数组中的每一个元素都执行一次,以返回该数据所对应的横坐标
+// 这里基于每个数据点(在数组中相应)的索引值并采用比例尺 x 进行映射,计算出相应的横坐标
+.x((d, i) => x(i))
+// 设置下边界线的纵坐标的读取函数
+// 这里基于每个数据点(二元数组)的第一个元素 d[0] 并采用比例尺 y 进行映射,计算出相应的纵坐标
+.y0(d => y(d[0]))
+// 设置上边界线的纵坐标的读取函数
+// 这里基于每个数据点(二元数组)的第二个元素 d[1] 并采用比例尺 y 进行映射,计算出相应的纵坐标
+.y1(d => y(d[1]));
+
+// 将每个系列的面积形状绘制到页面上
+const path = svg.selectAll("path") // 返回一个选择集,其中虚拟/占位元素是一系列的 路径元素,用于绘制各系列的形状
+ .data(randomize) // 绑定数据,每个路径元素 对应一个系列数据
+ .join("path") // 将元素绘制到页面上
+ // 由于面积生成器并没有调用方法 area.context(parentDOM) 设置画布上下文
+ // 所以调用面积生成器 area 返回的结果是字符串
+ // 该值作为 `` 元素的属性 `d` 的值
+ .attr("d", area)
+ // 设置填充颜色,为每个系列随机挑选一个颜色
+ // 使用 Math.random() 生成一个在 [0, 1) 范围里的随机数,然后通过方法 z() 从配色方案中得到相应的颜色值
+ .attr("fill", () => z(Math.random()));
+
+// 执行无限循环,不断更新河流图的数据
+(async function () {
+ while (true) {
+ // yield svg.node();
+ // 异步操作,在当前过渡完成时(Promise 才会 resolve),才会进入下一个循环周期(开始新一轮的过渡动画 )
+ await path
+ .data(randomize) // 绑定新生成的数据集
+ // 设置过渡动效(通过更改 `` 的属性 d 实现)
+ // 通过 selection.transition() 创建过渡管理器
+ // 过渡管理器和选择集类似,有相似的方法,例如为选中的 DOM 元素设置样式属性
+ // 具体参考官方文档 https://d3js.org/d3-transition
+ // 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-transition
+ .transition()
+ .delay(1000) // 设置过渡的延迟/等待时间
+ .duration(1500) // 设置过渡的时间
+ // 重绘各堆叠的面积形状
+ .attr("d", area)
+ // 最后通过方法 transition.end() 返回一个 Promise,仅在过渡管理器所绑定的选择集合的所有过渡完成时才 resolve
+ // 这样就可以在当前的过渡结束时,才做执行后面操作(重复下一轮动画)
+ .end();
+}
+})();
\ No newline at end of file
diff --git a/areachart/streamgraph-transition/index.html b/areachart/streamgraph-transition/index.html
new file mode 100644
index 0000000..9de7f37
--- /dev/null
+++ b/areachart/streamgraph-transition/index.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+ Streamgraph transition
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/index.html b/index.html
index b68f982..f0f10b2 100644
--- a/index.html
+++ b/index.html
@@ -464,6 +464,12 @@ {{ example.name }}
folder: 'shape-tweening',
note: 'https://datavis-note.benbinbin.com/article/d3/chart-example/d3-chart-example-area-chart#形状切换补间动画'
},
+ {
+ name: '河流图切换动效',
+ reference: 'https://observablehq.com/@benbinbin/streamgraph-transitions',
+ folder: 'streamgraph-transition',
+ note: 'https://datavis-note.benbinbin.com/article/d3/chart-example/d3-chart-example-area-chart#河流图切换动效'
+ },
]
},
{