如何基于D3.js将三级层级气泡图改造为四级可点击版本?
问题:如何将D3.js层级气泡图改造为四级可点击交互?
我用D3.js做了一个基于法国行政层级(国家→大区→省→城市)的气泡图,数据是四级结构的JSON:
{ "name": "France", "children": [ { "name": "Auvergne-Rhône-Alpes" }, { "name": "Bourgogne-Franche-Comté" }, { "name": "Bretagne", "children": [ {"name": "Côtes-d'Armor", "children": [ {"name": "ville1", "population": 31960}, {"name": "ville2", "population": 44021}, {"name": "ville3", "population": 23821}, {"name": "ville4", "population": 5819}, {"name": "ville5", "population": 30729}, {"name": "ville6", "population": 64913} ]} ]} ]}
目前我只实现了三级层级,且第二级和第三级会同时显示,现在需要改成四级可点击切换的交互:点击上级气泡展开下一级,同时支持返回上一级。现有代码如下:
const diameter = 700, // Rendre diameter adaptater à l'écran format = d3.format(",d"); const svg = d3.select("#chart").append("svg") .attr("width", diameter) .attr("height", diameter) .attr("id", "svg"); const g = svg.append("g").attr("transform", "translate(2,2)"); const pack = d3.pack() .size([diameter - 4, diameter - 4]) // Related to previous translation .padding(1.5); d3.json("donnee2.json").then(function(root) { root = d3.hierarchy(root) .sum(function(d) { return d.population; }) // La variable du JSON (à remplacer pour comparer la variable voulu) .sort(function(a, b) { return b.value - a.value; }) var node = g.selectAll("g") .data(pack(root).descendants()) .enter().append("g"); // Attribuer les classes aux "g" des cercles node.attr("class", function(d, i) { if (i > 0) return d.children ? "node" : "leaf_node"; }) .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }) .attr("text-anchor", "middle") .attr('fill', 'steelblue') .style("font", "10px sans-serif") .style("cursor", "pointer") .style("opacity", "0.6") .on("mouseover", function() { d3.select(this).attr("stroke", "white").style("stroke-width", "2px")}) .on("mouseout", function() { d3.select(this).attr("stroke", null)}); /********** HIERARCHY ***********/ // Attributes of circles in terms of hierarchy function first_scale(){ // Example: France node.filter(function(d, i) { if(i == 0) return d.children; }).append("circle") .attr("r", function(d) { return d.r; }) .attr("id", function(d, i) {return "b_"+i }) .on("dblclick", function() { second_scale()}); node.filter(function(d, i) { if(i == 0) return d.children; }).append("title") //afficher les données du JSON .text(function(d) { return d.data.name + "\n" + format(d.value) }) node.filter(function(d, i) { if(i == 0) return d.children; }).append("text") .attr("dy", "0.3em") .style('fill', 'white') .style("font", "30px sans-serif") .text(function(d) { return d.data.name.substring(0, d.r / 3); }); } first_scale(); function second_scale(){ // Example: les Régions node.filter(function(d, i) { if(i > 0) return d.children; }).append("circle") .attr("r", function(d) { return d.r; }) .attr("id", function(d, i) {return "b_"+i }) .on("dblclick", function() { third_scale()}); node.filter(function(d, i) { if(i > 0) return d.children; }).append("title") //afficher les données du JSON .text(function(d) { return d.data.name + "\n" + format(d.value) }) node.filter(function(d, i) { if(i > 0) return d.children; }).append("text") .attr("dy", "0.3em") .style('fill', 'white') .text(function(d) { return d.data.name.substring(0, d.r / 3); }); } function third_scale(){ // Example: les Départements node.filter(function(d) { return !d.children; }).append("circle") .attr("r", function(d) { return d.r; }) .attr("id", function(d, i) {return "b_"+i }) .on("click", function() { d3.select(this).attr("fill", "white")}); node.filter(function(d) { return !d.children; }).append("title") // Afficher les données du JSON .text(function(d) { return d.data.name + "\n" + format(d.value) }) node.filter(function(d) { return !d.children; }).append("text") .attr("dy", "0.3em") .style('fill', 'white') .text(function(d) { return d.data.name.substring(0, d.r / 3); }); } /* function fourth_scale(){} */ });

请问如何修改代码实现四级可点击的层级气泡图?
回答:
我来帮你重构代码解决这个问题——你的现有方案最大的问题是把层级切换拆成了多个独立函数,导致逻辑混乱,而且没有处理第四级的交互。我们可以用统一的状态管理+延迟渲染的方式,实现流畅的四级点击切换,还能支持返回上一级的功能,完美适配你的法国行政层级数据。
第一步:重构节点初始化逻辑
首先,我们先一次性创建所有节点的容器(g元素),但默认全部隐藏,只在需要显示的时候再渲染气泡和文字。这样避免了重复创建元素,也让层级切换更灵活:
const diameter = 700, format = d3.format(",d"); const svg = d3.select("#chart").append("svg") .attr("width", diameter) .attr("height", diameter); const g = svg.append("g").attr("transform", "translate(2,2)"); const pack = d3.pack() .size([diameter - 4, diameter - 4]) .padding(1.5); d3.json("donnee2.json").then(function(root) { // 构建层级结构并计算pack布局 root = d3.hierarchy(root) .sum(d => d.population) .sort((a, b) => b.value - a.value); const allNodes = pack(root).descendants(); // 创建所有节点的g容器,默认隐藏 const node = g.selectAll(".node") .data(allNodes) .enter() .append("g") .attr("class", d => d.children ? "parent-node" : "leaf-node") .attr("transform", d => `translate(${d.x}, ${d.y})`) .style("opacity", 0) .style("cursor", d => d.children ? "pointer" : "default") .on("mouseover", function() { d3.select(this).select("circle") .attr("stroke", "white") .style("stroke-width", 2); }) .on("mouseout", function() { d3.select(this).select("circle").attr("stroke", null); }); // 给所有节点添加tooltip(title元素) node.append("title") .text(d => `${d.data.name}\n${format(d.value)}`);
第二步:实现核心层级切换逻辑
我们用一个变量currentRoot记录当前显示的层级根节点,然后写一个通用的渲染函数,负责显示当前根节点和它的直接子节点,同时绑定点击事件展开下一级:
// 当前层级的根节点,初始为整个法国 let currentRoot = root; // 渲染指定层级的通用函数 function renderCurrentLevel() { // 先隐藏所有节点 node.style("opacity", 0); // 显示当前根节点(比如初始的法国,或者点击后的某个大区) node.filter(d => d === currentRoot) .style("opacity", 1) .each(function(d) { // 如果节点还没有气泡和文字,就创建 if (!d3.select(this).select("circle").node()) { d3.select(this).append("circle") .attr("r", d.r) .attr("fill", "steelblue"); d3.select(this).append("text") .attr("dy", "0.3em") .style("fill", "white") .style("font", d.depth === 0 ? "30px sans-serif" : "10px sans-serif") .text(d => d.data.name.substring(0, d.r / 3)); } }); // 显示当前根的子节点(下一级) const childNodes = currentRoot.children || []; node.filter(d => childNodes.includes(d)) .style("opacity", 1) .each(function(d) { if (!d3.select(this).select("circle").node()) { const circle = d3.select(this).append("circle") .attr("r", d.r) .attr("fill", "steelblue"); // 如果是父节点(还有下一级),绑定点击事件展开 if (d.children) { circle.on("click", () => { currentRoot = d; renderCurrentLevel(); addBackButton(); // 显示返回按钮 }); } d3.select(this).append("text") .attr("dy", "0.3em") .style("fill", "white") .text(d => d.data.name.substring(0, d.r / 3)); } }); }
第三步:添加返回上一级功能
为了让用户可以回溯层级,我们添加一个返回按钮,只有当不在最顶层时显示:
// 添加返回上一级按钮 function addBackButton() { // 先移除之前的按钮,避免重复创建 svg.select(".back-btn").remove(); if (currentRoot.depth > 0) { svg.append("text") .attr("class", "back-btn") .attr("x", 20) .attr("y", 30) .style("fill", "black") .style("font-size", "16px") .style("cursor", "pointer") .text("← 返回上一级") .on("click", () => { currentRoot = currentRoot.parent; renderCurrentLevel(); // 如果回到最顶层,移除返回按钮 if (currentRoot.depth === 0) svg.select(".back-btn").remove(); }); } } // 初始化渲染最顶层(法国) renderCurrentLevel(); });
为什么这个方案能解决你的问题?
- 适配四级结构:不管是国家→大区→省→城市,只要你的JSON是标准的层级结构,这个逻辑会自动识别所有层级,不需要为每一级写单独的函数
- 流畅的交互:点击父节点展开下一级,点击返回按钮回到上一级,逻辑清晰直观
- 性能优化:只有需要显示的节点才会渲染气泡和文字,避免一次性生成大量DOM元素
- 可扩展性:如果以后要添加更多层级,或者修改样式,只需要调整通用函数即可
额外的小优化建议
- 给不同层级的气泡设置不同颜色,比如国家用
#2c3e50,大区用#3498db,省用#1abc9c,城市用#ecf0f1,增强视觉区分度 - 调整
pack.padding()的值,让同级气泡之间的间距更合理 - 优化文本截断逻辑,比如用自定义函数确保文字不会超出气泡边界
内容的提问来源于stack exchange,提问作者Gabriel Chevallier
相关产品推荐
相关产品推荐

