如何遍历D3.js嵌套数据并实现分组Y轴水平条形图
实现递归遍历嵌套数据并构建分组Y轴的水平条形图
我来帮你搞定这个需求!针对你提供的d3.nest生成的嵌套数据,我们可以写一个通用的递归函数来遍历所有层级,收集分组信息,再基于这些信息构建多层Y轴分组的水平条形图。
首先先看你给出的嵌套数据:
let nestedData = [{ key: "A", values: [{ key: "ab", values: [{ key: "xy", values: [{ name: "tom" }] }, { key: "tu", values: [{ name: "corin" }] } ] }, { key: "cd", values: [{ key: "xy", values: [{ name: "nancy" }] }] } ] }, { key: "B", values: [{ key: "ab", values: [{ key: "xy", values: [{ name: "tom" }] }] }, { key: "cd", values: [{ key: "xy", values: [{ name: "nancy" }] }] } ] }]
第一步:编写递归处理函数
这个函数会遍历所有嵌套层级,收集每一级的分组键,同时记录最底层条目的完整路径(方便后续定位Y轴位置):
// 递归处理嵌套数据,收集层级分组信息和条目路径 function processNestedData(data, currentPath = [], level = 0) { const result = { groups: {}, // 存储每一层的所有分组key entries: [] // 存储最底层条目及其完整路径 }; // 记录当前层级的分组key(去重) result.groups[level] = [...new Set(data.map(item => item.key))]; data.forEach(item => { const newPath = [...currentPath, item.key]; // 如果还有子层级,递归处理 if (item.values && item.values.length) { const childResult = processNestedData(item.values, newPath, level + 1); // 合并子层级的分组信息 Object.keys(childResult.groups).forEach(lvl => { if (!result.groups[lvl]) { result.groups[lvl] = childResult.groups[lvl]; } else { result.groups[lvl] = [...new Set([...result.groups[lvl], ...childResult.groups[lvl]])]; } }); // 合并子层级的条目 result.entries.push(...childResult.entries); } else { // 到达最底层,保存条目和路径 result.entries.push({ ...item, path: newPath }); } }); return result; }
调用这个函数处理你的数据:
const processedData = processNestedData(nestedData);
第二步:构建Y轴比例尺(多层分组)
我们需要为每一层分组创建独立的比例尺,同时为最底层的条目创建基础Y轴比例尺:
// 先定义画布尺寸 const width = 600; const height = processedData.entries.length * 40; // 根据条目数动态计算高度 // 最底层条目的Y轴比例尺(用来定位每个条形的位置) const entryYScale = d3.scaleBand() .domain(d3.range(processedData.entries.length)) .range([0, height]) .padding(0.1); // 为每一层分组创建比例尺,用来计算分组标签的位置和范围 const groupYScales = {}; // 获取所有层级并按顺序排序 const levels = Object.keys(processedData.groups).map(Number).sort((a, b) => a - b); levels.forEach(level => { const groupKeys = processedData.groups[level]; // 计算每个分组包含的条目数量,用来确定分组在Y轴上的覆盖范围 const groupEntryCounts = groupKeys.map(key => { return processedData.entries.filter(entry => entry.path[level] === key).length; }); groupYScales[level] = d3.scaleBand() .domain(groupKeys) .range([0, height]) .padding(0.05); }); // X轴比例尺(假设我们给每个条目模拟一个数值) const xScale = d3.scaleLinear() .domain([0, 100]) .range([0, width - 200]); // 留出空间放多层Y轴标签
第三步:绘制分组Y轴标签和水平条形
现在我们可以把分组标签和条形画出来了:
// 创建SVG画布 const svg = d3.select("body") .append("svg") .attr("width", width) .attr("height", height); // 绘制每一层的分组标签 levels.forEach((level, idx) => { const scale = groupYScales[level]; svg.selectAll(`.group-label-${level}`) .data(scale.domain()) .enter() .append("text") .attr("class", `group-label-${level}`) .attr("x", -10 - idx * 80) // 每一层标签向左偏移,避免重叠 .attr("y", d => scale(d) + scale.bandwidth() / 2) .attr("dy", "0.35em") .attr("text-anchor", "end") .style("font-size", "14px") .text(d => d); }); // 绘制水平条形(这里给每个条目随机生成一个数值作为示例) svg.selectAll(".bar") .data(processedData.entries) .enter() .append("rect") .attr("class", "bar") .attr("x", 0) .attr("y", (d, idx) => entryYScale(idx)) .attr("width", () => xScale(Math.random() * 100)) .attr("height", entryYScale.bandwidth()) .attr("fill", "steelblue"); // 绘制X轴 const xAxis = d3.axisBottom(xScale); svg.append("g") .attr("transform", `translate(0, ${height})`) .call(xAxis);
关键说明
- 这个递归函数支持任意深度的嵌套数据,不管你的d3.nest分了多少层,都能自动适配
- 分组标签的偏移量可以根据实际需求调整,避免不同层级的标签重叠
- 如果你的实际数据里有数值字段(比如每个条目的value),直接替换掉示例里的随机数即可
- 可以通过调整
padding参数来控制分组和条形之间的间距
内容的提问来源于stack exchange,提问作者R.Kam
相关产品推荐
相关产品推荐

