tooltip = g => {
const formatTime = d3.utcFormat("%-I:%M %p");
const tooltip = g.append("g")
.style("font", "10px sans-serif");
const path = tooltip.append("path")
.attr("fill", "white");
const text = tooltip.append("text");
const line1 = text.append("tspan")
.attr("x", 0)
.attr("y", 0)
.style("font-weight", "bold");
const line2 = text.append("tspan")
.attr("x", 0)
.attr("y", "1.1em");
// 继续在 <text> 元素内添加一个 <tspan> 元素
const line3 = text.append("tspan")
.attr("x", 0)
// 纵向定位是 2.2em 相当于在第三行
.attr("y", "2.2em");
// 创建一个 <g> 元素,作为 voronoi 维诺图的容器
g.append("g")
// 由于所绘制的维诺图只用于交互,而不需要显示出来,所以填充颜色 fill 设置为 none
.attr("fill", "none")
// 允许指针事件(用户通过指针与维诺图进行交互)
.attr("pointer-events", "all")
// 使用 <path> 路径元素绘制维诺图
.selectAll("path")
// 绑定数据(stops 是各列车和各站点对应构建出来的数组,对应于图表上的数据点)
.data(stops)
.join("path") // 将一系列 <path> 元素(但是还没有设置具体的路径)挂载到容器中
// 为每个 <path> 元素设置属性 `d`(具体的路径形状)
// 通过调用维诺图生成器的方法 voronoi.renderCell(i) 绘制出第 i 格 cell 单元格
// 返回一个路径字符串(用作 svg 元素 <path> 的属性 d 的属性值)
// 关于维诺图生成器的相关代码解释可以查看 👇 后面的 cell
.attr("d", (d, i) => voronoi.renderCell(i))
// 通过方法 selection.on() 为选择集的元素设置事件监听器,以响应用户操作实现与图表的交互
// 为每个维诺图 cell 单元格设置 mouseout 事件监听器
// 当指针移离该单元格时,隐藏 tooltip
.on("mouseout", () => tooltip.style("display", "none"))
// 为每个维诺图 cell 单元格设置 mouseover 事件监听器
// 当指针悬浮在该单元格时,显示 tooltip
.on("mouseover", (event, d) => {
// ❓ null 是 display 属性的无效值,所以采用默认值(实际上继承自父元素,其实是 block ❓)
tooltip.style("display", null);
// 设置 tooltip 里的文本内容
// d 是当前鼠标所悬浮的维诺图 cell 单元格所绑定的数据
line1.text(`${d.train.number}${d.train.direction}`); // 第一行的内容是列车号码和列车的方向
line2.text(d.stop.station.name); // 第二行的内容是所停靠的车站名称
line3.text(formatTime(d.stop.time)); // 第三行是停靠时刻值(通过方法 formatTime() 进行格式化)
// 设置 tooltip 边框的描边颜色,通过 colors 对象进行映射得到与列车类型所对应的颜色值
path.attr("stroke", colors[d.train.type]);
// 使用方法 selection.node() 返回选择集第一个非空的元素,这里返回的是 <text> 元素
// 然后通过 SVGGraphicsElement.getBBox() 获取到该元素的大小尺寸
// 返回值是一个对象 {x: number, y: number, width: number, height: number } 表示一个矩形
// 这个矩形是刚好可以包裹该 svg 元素的最小矩形
const box = text.node().getBBox();
// 绘制 tooltip 边框,设置 <path> 元素的属性 `d`(具体路径形状)
// 命令 M 是将画笔移动到左上角
// 命令 H 绘制水平线,并在中间有一个小三角凸起(构成 tooltip 的指针形状,指向数据点)
// 命令 V 绘制垂直线
// 最终绘制出的 tooltip 边框,距离文本内容 10px(可以看作是 padding)
path.attr("d", `
M${box.x - 10},${box.y - 10}
H${box.width / 2 - 5}l5,-5l5,5
H${box.width + 10}
v${box.height + 20}
h-${box.width + 20}
z
`);
// 通过 CSS 的 transform 属性将 tooltip 「移动」到相应位置
// 其中横坐标值是基于 d.stop.station.distance(当前列车所停靠车站与第一个车站的距离)并通过横坐标轴比例尺 x 进行映射
// 纵坐标值是基于 d.stop.time(所停靠车站的时刻值)并提供纵坐标比例尺 y 进行映射
// 为了将 tooltip 的指针形状与数据点对齐,需要对横纵坐标进行「校正」调整
// 横坐标值偏移 box.width / 2 即 box 宽度的一半,纵坐标值偏移 28px 大概 3 行文字高度
tooltip.attr("transform", `translate(${
x(d.stop.station.distance) - box.width / 2},${
y(d.stop.time) + 28
})`);
});
}