Public
Edited
Mar 2, 2024
Insert cell
Insert cell
chart = {
// 创建 svg(返回的是一个包含 svg 元素的选择集)
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height); // 基于可以最多的节点来设置高度值

// 横坐标轴的定义域
// 初始状态是载入根节点的数据,即从一条(根节点对应的)柱子(使用过渡动效)展开为多条(子节点)柱子
// 所以定义域的上界采用的是根节点的数据
x.domain([0, root.value]);

// 在 svg 中添加一个 <rect> 矩形元素作为背景
svg.append("rect")
.attr("class", "background") // 为元素添加 background 类名
.attr("fill", "none")
// 因为上一步将 <rect> 矩形元素的填充元素设置为 none 所以需要设置 pointer-events
// 通过设置 CSS 属性 pointer-events 为 all 使 svg 元素会成为鼠标事件的目标
// 即使 fill 属性设置为 none 也不影响事件处理(属性 stroke 和 visibility 也不影响)
.attr("pointer-events", "all") // 让矩形元素允许所有的点击事件
.attr("width", width)
.attr("height", height)
.attr("cursor", "pointer") // 设置鼠标 hover 在矩形元素上时的指针样式
// 在矩形元素上面添加点击事件监听器
// 当用户点击条形图的背景时,会调用函数 up(返回数据的上一级)
// 其中回调函数的第二个参数 d 是 <rect> 矩形元素所绑定的数据
.on("click", (event, d) => up(svg, d));

// 绘制横坐标轴
svg.append("g")
.call(xAxis); // 调用相应的方法,将坐标轴在相应容器内部渲染出来

// 绘制纵坐标轴
svg.append("g")
.call(yAxis);

// 调用 down 函数,传入的数据是根节点,绘制初始化的条形图
// 初始状态是载入根节点的数据,即从一条(根节点对应的)柱子(使用过渡动效)展开为多条(子节点)柱子
down(svg, root);

return svg.node();
}
Insert cell
// 在画面中绘制条形图的柱子
// 传入的参数依次是:
// * svg 只包含一个 svg 元素的选择集
// * down 柱子被点击后时调用的函数(数据下钻)
// * d 基于哪个节点的子节点数据构建条形图的柱子,
// 如果是在数据下钻时调用该函数,就传入被点击的柱子所对应/所绑定的节点
// 如果是在返回上一级数据时调用该函数,就传入需要作为 svg 的背景 <rect> 元素所绑定的数据
// * selector 新增的元素会放置在该选择器所命中的元素之前
// svg 元素显示层叠遮挡与它们在 DOM 结构树中的顺序相关,可以通过调整元素的顺序避免被遮挡
function bar(svg, down, d, selector) {
// 在 svg 中添加一个 <g> 元素作为条形图中新增柱子的(大)容器
// 而且该新增的元素会放置在 selector 选择器所匹配的元素之前
const g = svg.insert("g", selector)
.attr("class", "enter") // 为该容器添加上 enter 类名
// 通过设置 CSS 的 transform 属性将容器移动到合适的位置
// 距离上方的横坐标轴有一定的距离
.attr("transform", `translate(0,${margin.top + barStep * barPadding})`)

// 为每个柱子设置一个(小)容器,并绑定数据,以及设置点击事件监听器
const bar = g.selectAll("g")
// 将被点击的柱子所绑定的节点(数据)的子节点作为各新增柱子所绑定的数据
.data(d.children)
.join("g")
// 根据所绑定的节点是否为叶子节点(没有子节点)来设置不同的鼠标指针样式
.attr("cursor", d => !d.children ? null : "pointer")
// 为各柱子添加点击事件监听器
// 柱子被点击后调用 down() 函数,并传入 svg 和柱子所绑定的节点(数据)
.on("click", (event, d) => down(svg, d));

// 为各柱子设置标注信息
bar.append("text")
// 通过设置 (x, y) 属性设置文字元素的定位
// 因为文字和每个柱子都是在同一个(小)容器内的
// 所以这里的 (x, y) 设置的是(与容器的)相对定位,以便和矩形元素有一定的间隙
.attr("x", margin.left - 6)
.attr("y", barStep * (1 - barPadding) / 2)
.text(d => d.data.name) // 标注内容
// 设置一些文字相关样式
.attr("dy", ".35em")
.attr("text-anchor", "end") // 文字对齐方式采用 end
.style("font", "10px sans-serif");

bar.append("rect")
// 为各矩形元素设置相关的尺寸和定位属性
// 设置矩形的横坐标,都是对齐到横轴的零点位置
// 💡 这里设置的是矩形的最终定位,即各柱子矩形的左上角的 x 坐标是在零点位置
// 不过在后面的 down 函数中紧接着再通过 CSS 的 transform 属性,为各新增柱子设置不同的横向偏移将它们堆叠起来
.attr("x", x(0))
// 没有设置矩形的纵坐标(相当于采用默认值 0),在过渡动效中才进行设置
// 设置矩形柱子的横向长度
// 下钻时是先基于旧的横坐标轴比例尺,所以在 down() 函数最后还要重新计算新增柱子的长度
.attr("width", d => x(d.value) - x(0))
.attr("height", barStep * (1 - barPadding)); // 设置矩形柱子的带宽

return g; // 返回这个包含新增柱子的容器
}
Insert cell
// 让柱子沿着横轴堆叠起来,形成一条矩形柱子
// 入参 i 就是点击的柱子(容器)所绑定的(节点)数据里的 d.index 序号
function stack(i) {
// 作为 d.value 的累计值
// 即当前的 value 值是前面已经遍历的矩形柱子元素所绑定的(节点)数据的 d.value 值的和
let value = 0;
// 最后返回一个函数,该函数才是选择集中的每个元素所调用的
// 这是为了使用闭包,保留 value 变量,然后在遍历柱子矩形元素时,可以基于 value 变量计算出当前所遍历的柱子的横向偏移量 x(value) - x(0)
// 不断增大的 value 变量可以让柱子产生不同的横向的偏移(是前面柱子长度的总和),让柱子不必重叠在一起,而是堆叠在一起
// 而柱子在纵向偏移量是一样的,都是基于 i 算出的 barStep * i 即所有的新增的柱子与点击的柱子在同一水平线上
return d => {
const t = `translate(${x(value) - x(0)},${barStep * i})`;
value += d.value;
return t; // 该函数最后返回一个字符串,作为 CSS 的 transform 属性的值
};
}
Insert cell
// 让柱子沿着纵轴堆展开
function stagger() {
let value = 0;
// 和 stack 方法一样,使用闭包,保留 value 变量,基于 value 变量计算出当前所遍历的柱子的横向偏移量 x(value) - x(0) 不断增大的 value 变量可以让柱子产生不同的偏移(是前面柱子长度的总和)
// 而柱子在纵向的偏移量则是基于它们在选择集中的索引值 i 计算出来的
// 这样可以让展开的柱子呈阶梯状
return (d, i) => {
const t = `translate(${x(value) - x(0)},${barStep * i})`;
value += d.value;
return t;
};
}
Insert cell
// 点击柱子时该函数会被调用
// 实现数据下钻
// 传入的参数依次是:
// * svg 只包含一个 svg 元素的选择集
// * d 被点击的柱子所绑定的数据(节点),数据下钻后条形图所展示的就是该节点的子节点
function down(svg, d) {
// 如果该节点没有子节点或存过正在执行的过渡动效(即上一个过渡动效还没有执行结束)就直接返回,不执行余下的操作
// 其中所使用的选择集的方法 selection.node() 会返回选择集第一个非空的元素,如果选择集为空则返回 null
// 而方法 d3.active(node) 获取指定元素正在执行中的过渡管理器
if (!d.children || d3.active(svg.node())) return;

// 更新 svg 背景 <rect> 元素所绑定的数据
// 将当前下钻(所点击)的节点对象作为数据绑定到 svg 背景上
svg.select(".background").datum(d);

// (在 svg 上)定义两个先后执行的过渡管理器
const transition1 = svg.transition().duration(duration);
// 可以使用 transition.transition() 方法对同一个选择集设置一系列依次执行的过渡动效
// 第二个过渡管理器 transition2 会基于原有的过渡管理器创建一个新的过渡管理器,它所绑定的选择集相同,而且继承了原有过渡的名称、时间、缓动函数等配置
// 而且(通过第二个过渡管理器所创建的)过渡会在通过前一个过渡管理器所创建的过渡**结束后**才会开始执行
const transition2 = transition1.transition();
/**
* 移除条形图中原有的矩形柱子
*/
// 为画面当前的条形图柱子的(大)容器 <g>(具有 enter 类名)添加 exit 类名标记
// 表示当前条形图中的柱子都将会被移除
const exit = svg.selectAll(".enter")
.attr("class", "exit");

// 在容器中选择所有的矩形
// 其中被点击的矩形柱子(并不做过渡动效)立即隐藏,因为数据下钻 enter 新增的 <rect> 元素会立即显示
// 避免出现两个矩形元素重叠在一起
exit.selectAll("rect")
// 通过比对当前的矩形元素所绑定的数据(节点)p 和传入的数据(节点)d 来判断是否为点击的元素
// 通过设置透明度为 0 立即隐藏被点击的柱子
.attr("fill-opacity", p => p === d ? 0 : null);

// 然后为即将移除的柱子的(大)容器添加一个淡出的过渡动效,将透明度从 100% 变成 0
// 使用过渡管理器 transition1 的配置,为该过程该过程创建一个过渡
exit.transition(transition1)
.attr("fill-opacity", 0)
.remove(); // 最后再移除元素

/**
* 在条形图上添加新的矩形柱子
*/
// 调用 bar 函数,在条形图中绘制出新的柱子
// 传入的依次的参数依次是:
// * svg 只包含一个 svg 元素的选择集
// * down 作为新增柱子被点击后时调用的函数
// * d 被点击的柱子所绑定的节点
// 返回一个包含新增柱子的 <g> 容器
// * .y-axis 作为选择器,新增的元素会放置在纵轴元素之前,这样就可以避免新增的柱子矩形元素遮挡纵轴
const enter = bar(svg, down, d, ".y-axis")
.attr("fill-opacity", 0) // 先将容器透明度设置为 0
// 但是这里并没有隐藏柱子矩形,只隐藏文字
// 可以看该函数最后部分,新增的柱子矩形元素单独设置了透明度 100%,即它们会被立即添加到页面上
// 然后为即将添加的柱子的(大)容器添加一个淡入的过渡动效
// 使用过渡管理器 transition1 的配置,为容器创建一个过渡,将透明度从 100% 变成 0
// 实际效果是让容器里的文字(柱子的标注信息)淡入
enter.transition(transition1)
.attr("fill-opacity", 1);

// 设置新增柱子(各小容器)的过渡动效
enter.selectAll("g")
// 先将新增的柱子沿着横轴堆叠起来,且在纵向与点击的柱子的位置一样
// 需要注意选择集中的每个元素所调用的并不是 stack(d.index) 而是它所返回的函数
// 返回的函数只接收了一个入参 d 即每个元素所绑定的数据
.attr("transform", stack(d.index))
// 然后沿着纵轴将其展开(使用过渡管理器 transition1 的配置,为该过程创建一个过渡)
.transition(transition1)
// 需要注意选择集中的每个元素所调用的并不是 stagger() 而是它所返回的函数
// 返回的函数接收了两个入参 d(每个元素所绑定的数据)和 i(当前所遍历的元素在选择集中的索引值)
.attr("transform", stagger());

// 更新横坐标轴的定义域
// 其上界采用(点击的节点的)子节点数据里的最大值
x.domain([0, d3.max(d.children, d => d.value)]);

// (采用新的定义域)重新绘制横坐标轴
// 使用过渡管理器 transition2 的配置,为该过程创建一个过渡,让横坐标轴更新有一个顺滑的视觉动效
// 所以这个过渡动效会在 transition1 完成后再执行
svg.selectAll(".x-axis").transition(transition2)
.call(xAxis);

// 将新增的呈阶梯状的柱子矩形(小容器)移回到纵坐标轴处
// 通过调整各新增柱子(小容器)的 CSS 属性 transform,将横向偏移量改回为 0
// 使用过渡管理器 transition2 的配置,为该过程创建一个过渡
// 所以这个过渡动效会在 transition1 完成后再执行
enter.selectAll("g").transition(transition2)
.attr("transform", (d, i) => `translate(0,${barStep * i})`);

// 设置新增的柱子的颜色和长度
enter.selectAll("rect")
.attr("fill", color(true)) // 新增的柱子的颜色先全都设置为蓝色
// 这里将新增的柱子的透明度设置为 100%
// 所以在前面将包含新增柱子的(大)容器的的透明度设置为 0,而在这里则会被覆盖
// 即新增的柱子实际上会立即添加到页面上并显示出来,没有应用到过渡动效,只对标注信息文字应用了淡入动效
.attr("fill-opacity", 1)
// 最后设置新增的柱子的颜色和长度
// 使用过渡管理器 transition2 的配置,为该过程创建一个过渡
// 所以这个过渡动效会在 transition1 完成后再执行
.transition(transition2)
// 新增的柱子会基于其所绑定的节点(数据)是否具有子节点来设置不同的颜色
.attr("fill", d => color(!!d.children))
.attr("width", d => x(d.value) - x(0)); // 基于新的横坐标轴比例尺,重新计算矩形的长度
}
Insert cell
// 点击条形图的背景时该函数会被调用
// 返回上一级数据
// 该过程和 down() 函数内的过程相反
// 所以各可视元素更新顺序及其所采用的过渡顺序是相反的
// 传入的参数依次是:
// * svg 只包含一个 svg 元素的选择集
// * d 是条形图背景元素 <rect> 所绑定的节点(数据)
// 该节点是条形图当前显示的柱子所对应的节点的**父节点**
function up(svg, d) {
// 如果该节点没有父节点(即根节点)或条形图中还有未移除的柱子(相当于上一个过渡动效还没有执行结束)就直接返回,不执行余下的操作
// 其中 selection.empty() 返回一个布尔值,以表示选择集是否为空,即其中不包含元素
// 通过该该方法判断是否已经将需要移除的柱子(的容器)删除掉
if (!d.parent || !svg.selectAll(".exit").empty()) return;
// 其中可以使用 down() 函数的判断条件,通过方法 d3.active(node) 获取指定元素正在执行中的过渡管理器,更准确地判断当前是否存过正在执行的过渡动效
// if (!d.parent || d3.active(svg.node())) return;
// 更新 svg 背景 <rect> 元素所绑定的数据
// 将原来所绑定的节点对象的父节点作为数据绑定到 svg 背景上
svg.select(".background").datum(d.parent);

// (在 svg 上)定义两个先后执行的过渡管理器
const transition1 = svg.transition().duration(duration);
// (通过第二个过渡管理器所创建的)过渡会在通过前一个过渡管理器所创建的过渡**结束后**才会开始执行
const transition2 = transition1.transition();

/**
* 移除条形图中原有的矩形柱子
*/
// 为画面当前的条形图柱子的(大)容器 <g>(具有 enter 类名)添加 exit 类名标记
// 表示当前条形图中的柱子都将会被移除
const exit = svg.selectAll(".enter")
.attr("class", "exit");

// 更新横坐标轴的定义域
// 其上界采用同级节点的 d.parent.children 数据里的最大值
x.domain([0, d3.max(d.parent.children, d => d.value)]);

// (采用新的定义域)重新绘制横坐标轴
// 使用过渡管理器 transition1 的配置,为该过程创建一个过渡,让横坐标轴更新有一个顺滑的视觉动效
svg.selectAll(".x-axis").transition(transition1)
.call(xAxis);
// 调整即将要被移除的柱子的 CSS 的 transform 属性的值,为这些柱子设置不同的横向偏移,让它们呈阶梯状
// 使用过渡管理器 transition1 的配置,为该过程创建一个过渡
exit.selectAll("g").transition(transition1)
.attr("transform", stagger());

// 采用新的横轴比例尺重新计算这些要移除的柱子的长度,而且将它们的颜色都变成蓝色
// 使用过渡管理器 transition1 的配置,为该过程创建一个过渡
// 所以该过渡和上一步的操作,让这些将要移除的柱子横向偏移呈阶梯状的过程同时进行
exit.selectAll("rect").transition(transition1)
.attr("width", d => x(d.value) - x(0))
.attr("fill", color(true));
// 再退一步,让这些柱子堆叠在一起成为一条长的矩形,且在纵向与原来下钻时点击的柱子的位置一样
// 调整即将要被移除的柱子的 CSS 的 transform 属性的值,让它们的纵向偏移都恢复为 0(在同一条水平线上)
// 使用过渡管理器 transition2 的配置,为该过程创建一个过渡
// 所以这个过渡动效会在 transition1 完成后再执行
exit.selectAll("g").transition(transition2)
.attr("transform", stack(d.index));

// 然后为即将移除的柱子的(大)容器添加一个淡出的过渡动效,将透明度从 100% 变成 0
// 使用过渡管理器 transition2 的配置,为该过程该过程创建一个过渡
exit.transition(transition2)
.attr("fill-opacity", 0)
.remove(); // 最后再移除元素

/**
* 在条形图上添加新的矩形柱子
*/
// 调用 bar 函数,在条形图中绘制出新的柱子
// 传入的依次的参数依次是:
// * svg 只包含一个 svg 元素的选择集
// * down 作为新增柱子被点击后时调用的函数
// * d.parent 作为 svg 的背景 <rect> 元素所绑定的数据
// 返回一个包含新增柱子的 <g> 容器
// * .exit 作为选择器,新增的元素会放置在需要移除的柱子的(大)容器之前,这样就可以避免遮挡住移除柱子过程中的过渡动效
const enter = bar(svg, down, d.parent, ".exit")
.attr("fill-opacity", 0); // 先将容器透明度设置为 0

// 通过调整各新增柱子(小容器)的 CSS 属性 transform
// 将新增的柱子沿纵轴分布好
enter.selectAll("g")
.attr("transform", (d, i) => `translate(0,${barStep * i})`); // barStep 是柱子的带宽

// 然后将显示新增柱子的(大)容器透明度设置回 100% 将它们显示出来
// 使用过渡管理器 transition2 的配置,为该过程创建一个淡入的过渡动效
enter.transition(transition2)
.attr("fill-opacity", 1);

// 设置新增的柱子的颜色
// 并单独处理与之前点击下钻时处于同一位置的新增的柱子
// Exiting nodes will obscure the parent bar, so hide it.
// Transition entering rects to the new x-scale.
// When the entering parent rect is done, make it visible!
enter.selectAll("rect")
// 新增的柱子会基于其所绑定的节点(数据)是否具有子节点来设置不同的颜色
.attr("fill", d => color(!!d.children))
// 因为在 transition2 过渡执行完成之前,即将移除的柱子还显示在页面
// 所以这里将对应的新增柱子的透明度设置为 0 先将其隐藏起来
// 等到 transition2 过渡执行结束后,再显示出来
.attr("fill-opacity", p => p === d ? 0 : null)
// 使用过渡管理器 transition2 的配置创建一个过渡
.transition(transition2)
// 基于新的横坐标轴比例尺,计算新添加的柱子矩形的长度
// 其实这一步是可以省略的,因为与 down() 函数不同(它是先构建出新增的柱子,再更新横坐标轴的定义域),因为在该函数中,是先更新横坐标轴的定义域再去构建新增的柱子
.attr("width", d => x(d.value) - x(0))
// 等到 transition2 过渡执行结束后,将所有的矩形元素的透明度都设置为 100%
// 所以前面隐藏起来的哪个柱子也会显示出来
// 在过渡事件 end 监听器的回调函数中 this 表示当前所遍历的 DOM 元素
// 通过 d3.select(this) 可以基于传入的 DOM 元素构建一个选择集,这样就可以使用 D3 的链式方法,例如 selection.attr() 为选择集中的元素设置属性
.on("end", function(p) { d3.select(this).attr("fill-opacity", 1); });
}
Insert cell
data = FileAttachment("flare-2.json").json() // 读取并解析数据
Insert cell
// 通过方法 d3.hierarchy() 对数据进行处理,基于树形数据 data 计算层级结构
// 构建出各层级的节点(并为节点添加相应的属性)
root = d3.hierarchy(data)
// node.sum() 方法为所有节点分别添加 node.value 属性
// 该示例中 node.value 表示当前节点所在分支后的所有叶子节点所绑定的值的**累计值**
// 具体作用可以参考官方文档 https://github.com/d3/d3-hierarchy#node_sum
.sum(d => d.value)
// node.sort() 方法对同层级的节点进行排序
// 这里是按照前面算到的各节点的累计值 node.value 进行降序 descending 排序
// 具体作用可以参考官方文档 https://github.com/d3/d3-hierarchy#node_sort
.sort((a, b) => b.value - a.value)
// node.eachAfter(callback) 方法以后序遍历的顺序让节点均调用一次函数回调函数 callback
// 该示例中(从叶子节点开始)为所有节点添加一个属性 index
// 用于下钻时的动效过渡,作为同层级的节点的的唯一标识符,以便在过渡动效中实现柱子的堆叠和交错的效果
// index 的值根据节点是否具有父节点 d.parent 来决定
// 如果有父节点则将父节点的 index 值加一,同时将此时的父节点的 index 值作为该子节点的 index 值(如果父节点本来没有 index 值,则初始值为 0,所以此时所遍历的子节点的 index 也是 0)
// 如果没有父节点(根节点)则它的 index 值为 0
// 💡 因为是后序遍历,所以在遍历一个分支上的子节点时,所对应的那个「父节点」index 其实是作为一个「指针」,临时记录所遍历的当前的子节点的 index 值,让下一个所遍历的子节点可以根据前一个子节点的 index 值依次递增 1
// 等到子节点遍历完成后,这个「父节点」和同层级的节点才会进入遍历,它的 index 值就会被重新设置,根据是否有父节点 parent 和在同级节点中的位置(遍历的顺序)来设置 index 的值
.eachAfter(d => {
d.index = d.parent ? (d.parent.index = d.parent.index + 1 || 0 ) : 0
})
Insert cell
// 横轴比例尺
// 横坐标轴的数据是连续型的数值,默认使用 d3.scaleLinear 构建一个线性比例尺
// 其中横坐标轴的值域(可视化属性,这里是页面的宽度,不包括左右的留白)范围 [left, right]
x = d3.scaleLinear().range([margin.left, width - margin.right])
Insert cell
// 绘制横坐标轴的函数
// 接受一个容器 <g> 并在其中绘制出横坐标轴
xAxis = g => g
.attr("class", "x-axis") // 为容器添加一个 x-axis 类名
// 通过设置 CSS 的 transform 属性将横坐标轴容器定位到顶部
.attr("transform", `translate(0,${margin.top})`)
// 横轴是一个刻度值朝上的坐标轴
// 而且设置了刻度线的(建议)数量和刻度值的数字格式化说明符 specifier(用于格式化坐标轴的刻度值)
// 关于数字格式化说明符 specifier 可以参考官方文档 https://github.com/d3/d3-format
// 这里的 "s" 是指采用国际单位制词头 International System of Units (SI) prefix 来表示单位的倍数和分数
// 先将原数值四舍五入到有效数字,然后再采用字母来表示数值,例如使用 k 表示「千」的单位
// 这样刻度值就可以表示较大的数值
// 具体可以参考这里 https://mathworld.wolfram.com/SIPrefixes.html
.call(d3.axisTop(x).ticks(width / 80, "s"))
// 删掉坐标轴的轴线(它含有 domain 类名)
// 先通过一个三元运算符进行判断 g 是否具有 selection 方法
// 这是为了兼容在过渡动效中更新横向坐标轴的场景
// 在初始化时,该函数传入参数 g 是一个只包含 <g> 元素(坐标轴的容器)的选择集,可以直接调用 g.select(".domain") 方法
// 而条形图在过渡动效中,该函数传入参数 g 是过渡对象 transition,它具有 transition.selection 方法
// 方法 transition.selection() 获取该过渡管理器所绑定的选择集,再调用 .select(".domain") 方法对选择集进行二次选择,选择满足条件的元素(坐标轴线),构成一个新的选择集
// 最后通过 .remove() 方法移除选择集中的元素(坐标轴线)
.call(g => (g.selection ? g.selection() : g).select(".domain").remove())
// 其中有一个方法可以实现预想效果,就是通过将轴线的透明度设置为 0 即可
// 因为过渡对象 transition 也有 attr 方法,可以对它所绑定的选择集里的元素设置样式
// .call(g => g.select(".domain").attr('opacity', 0))

Insert cell
// 绘制纵坐标轴的函数
// 只需要在初始化条形图时调用一次
// 其作用只是在 svg 的左侧绘制一条直线(并没有构建比例尺、刻度值等)
yAxis = g => g
.attr("class", "y-axis") // 为容器添加一个 y-axis 类名
// 通过设置 CSS 的 transform 属性将纵坐标轴容器定位到左侧
.attr("transform", `translate(${margin.left + 0.5},0)`)
// 绘制直线,通过 <line> 元素
.call(g => g.append("line")
.attr("stroke", "currentColor")
// 设置直线的起始点坐标(只设置纵坐标,横坐标采用默认值 x1=0)
.attr("y1", margin.top)
// 设置直线的终止点坐标(只设置纵坐标,横坐标采用默认值 x1=0)
.attr("y2", height - margin.bottom))
Insert cell
// 分类比例尺
// 将离散的数据映射为不同的颜色
// 在该示例中将当前节点是否具有子节点的两个状态 [true, false](定义域)映射为两种颜色 ["steelblue", "#aaa"](值域)
// 即当条形图中柱子所对应的节点具有子节点时(可下钻)则设置为蓝色,无子节点(不可下钻)则设置为灰色
color = d3.scaleOrdinal([true, false], ["steelblue", "#aaa"])
Insert cell
barStep = 27 // 柱子的高度(带宽)
Insert cell
barPadding = 3 / barStep // 每个相邻柱子之间的间隔系数(比例)
Insert cell
duration = 750 // 过渡动效的持续时间
Insert cell
// svg 的高度
// 基于树形数据中同层级**节点数量最多**的场景来设置高度值
// 这样即使在数据下钻到最多节点的情况,条形图也可以显示完全的柱子
height = {
// max 是在树形数据的同一个层级中最多的节点数量
let max = 1; // 初始值
// 使用 node.each() 方法以广度优先的顺序依次遍历所有节点,依次调用传入的回调函数
// 最后 max 获取同一个层级中最多的节点数量
root.each(d => d.children && (max = Math.max(max, d.children.length)));
// max 最大值是 32
// 从根节点到该最多子节点的路径:flare -> query -> methods
return max * barStep + margin.top + margin.bottom; // 基于 max 计算出 svg 的高度值
}
Insert cell
// 在 svg 四边留白,构建一个显示的安全区,以便在四周显示坐标轴
margin = ({top: 30, right: 30, bottom: 0, left: 100})
Insert cell
d3 = require("d3@7")
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