D3.js:如何为一维数组元素创建父元素及多子SVG元素?
我在处理一维数组时,无法为每个数据条目创建group(g)元素及两个子SVG组件(text和rect)。allCorrespondingYValues是类似[1,2,3,4]的数组,相关代码如下:
selection.selectAll("g.y-crosshair-tooltip").data(allCorrespondingYValues) .join('g') .attr("class", "y-crosshair-tooltip") .attr("transform", d => "translate(" + (width - margin.right) + "," + (yScale(d)) + ")") //.select("text").join('text') // Method 1: Nothing appears, just 1 'g' per data //.join("text") // Method 2: Nothing appears, just 1 'g' per data //.append("text") // Method 3: this will continuously append a new text element inside the parent 'g' every time the crosshair moves and the function this code is contained in is called //.selectAll("text").data(d => d).join("text") // Method 4: Throws errors. .selectAll("text").data([null]).join("text") // Method 5: This works and creates only 1 child text element for the 'g', but the text is always null because its referencing the null dataset, not the parent dataset. .attr("dx", -15) // position relative to its group .attr("dy", -15) .attr("fill", "black") .text(d => "$" + d)
我尝试了上述五种方法,注释中说明了每种方法的问题。我对方法1无效感到困惑,因为Mike的选择指南提到select会将数据从父元素传播到子元素,但使用.select("text").join('text')后,父g元素中并未添加任何内容。
我的两个问题:
- 为什么方法1无效?
- 为每个数据元素创建父元素和多个子元素的最佳实践是什么?
我找到过类似问题,但大多使用enter()而非join(),或针对二维数组,而我需要从父元素引用相同的数据。
解答
1. 方法1无效的原因
.select("text")会从每个父g元素中查找已存在的text元素,但初始状态下这些g都是新创建的(join('g')的enter部分),内部没有text元素,所以select返回的是空选择集。此时调用.join('text'),因为没有匹配的现有元素,也没有绑定数据到这个空选择集,所以不会创建任何text元素。
核心点:select不会自动为子元素绑定父元素的数据,join需要选择集绑定数据才能创建元素,方法1中select("text")得到的空选择集没有绑定数据,自然无法生成子元素。
2. 最佳实践:为每个父元素创建多子组件的正确方式
针对每个父g元素需要固定数量的子组件(比如1个text+1个rect),推荐两种方式:
方式一:链式传递父元素数据
利用join返回的父g选择集,通过data([d])将父元素的数据包装为数组传递给子元素,配合selectAll+join完成子组件的创建与更新:
selection.selectAll("g.y-crosshair-tooltip") .data(allCorrespondingYValues) .join('g') .attr("class", "y-crosshair-tooltip") .attr("transform", d => `translate(${width - margin.right}, ${yScale(d)})`) // 处理text子元素 .selectAll("text") .data(d => [d]) // 将父元素数据传递给子元素 .join("text") .attr("dx", -15) .attr("dy", -15) .attr("fill", "black") .text(d => "$" + d) // 返回父g选择集,处理rect子元素 .select(function() { return this.parentNode; }) .selectAll("rect") .data(d => [d]) .join("rect") .attr("width", 50) .attr("height", 20) .attr("x", -60) .attr("y", -20) .attr("fill", "white") .attr("stroke", "black");
方式二:用each遍历父元素操作
如果子组件逻辑复杂,可通过each遍历每个父g元素,直接在内部独立处理子组件的创建与更新:
selection.selectAll("g.y-crosshair-tooltip") .data(allCorrespondingYValues) .join('g') .attr("class", "y-crosshair-tooltip") .attr("transform", d => `translate(${width - margin.right}, ${yScale(d)})`) .each(function(d) { // 处理当前g内的text d3.select(this) .selectAll("text") .data([d]) .join("text") .attr("dx", -15) .attr("dy", -15) .attr("fill", "black") .text("$" + d); // 处理当前g内的rect d3.select(this) .selectAll("rect") .data([d]) .join("rect") .attr("width", 50) .attr("height", 20) .attr("x", -60) .attr("y", -20) .attr("fill", "white") .attr("stroke", "black"); });
这两种方式都能确保每次更新时,每个父g下仅保留对应子元素,不会重复创建,同时正确继承父元素的数据。
内容的提问来源于stack exchange,提问作者louis

