D3.js分组柱状图切换数据时旧图表未移除问题求助
D3.js分组柱状图切换时旧元素叠加问题的解决方案
问题背景
实现分组柱状图展示测试题作答表现,支持两组CSV数据切换。首次加载数据正常,但切换时新图表会叠加在旧图表之上,旧元素未被移除。
用户原实现代码:
// set the dimensions and margins of the graph const margin = {top: 10, right: 30, bottom: 20, left: 50}, width = 460 - margin.left - margin.right, height = 400 - margin.top - margin.bottom; // append the svg object to the body of the page let svg = d3.select("#my_dataviz") .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform",`translate(${margin.left},${margin.top})`); function update (test_no){ // Reading Data from CSV d3.csv(`bardisect_${test_no}.csv`).then( function(data) { // Creates the subgroups using group names let subgroups = data.columns.slice(1) // Making a map of the subgroups for the x axis let groups = data.map(d => d.groups) // Add X axis let x = d3.scaleBand() .domain(groups) .range([0, width]) .padding([0.2]) svg.append("g") .attr("transform", `translate(0, ${height})`) .call(d3.axisBottom(x).tickSize(0)); // Add Y axis let y = d3.scaleLinear() .domain([0, 100]) .range([ height, 0 ]); svg.append("g") .call(d3.axisLeft(y)); // creating separate x axis for subgroups let xSubgroup = d3.scaleBand() .domain(subgroups) .range([0, x.bandwidth()]) .padding([0.05]) // colours for the subgroups let color = d3.scaleOrdinal() .domain(subgroups) .range(['green','red','grey']) // Show the bars svg.append("g") .selectAll("g") // Looping through each group to shoow the right numbers .data(data) .join( enter => { enter let sel = enter .append("g") .attr("transform", d => `translate(${x(d.groups)}, 0)`) return sel; }) .selectAll("rect") .data((d) => { return subgroups.map(function(key) { return {key: key, value: d[key]}; }); }) .join( (enter) => { enter .append("rect") .attr('fill', 'white') .attr("x", d => xSubgroup(d.key)) .attr("y", d => y(d.value)) .attr("width", xSubgroup.bandwidth()) .attr('height', 0) .transition() .duration(1000) .attr("height", d => height - y(d.value)) .attr("fill", d => color(d.key)) }, (update) => { update .transition() .duration(1000) .attr("fill", d => color(d.key)) .attr("x", d => xSubgroup(d.key)) .attr("width", xSubgroup.bandwidth()) .attr("height", d => height - y(d.value)) .attr("y", d => y(d.value)) }, (exit) => { exit .transition() .duration(1000) .attr('height', 0) .remove(); } ) }); } //updating the test selection let select = d3.select('#test_no'); select.on('change', function() { console.log(this.value); update(this.value); }) update('02');
问题原因
- 每次调用
update函数时,都会通过svg.append("g")新增坐标轴容器和柱状图容器,旧的容器及内部元素未被移除,导致叠加 join方法仅处理了rect元素的更新/移除,但外层的group元素是每次新创建的,旧group没有被清理
修复方案
方案一:提前创建容器并复用(推荐)
初始化时为坐标轴、柱状图创建固定容器,后续更新时直接复用这些容器,避免重复创建:
初始化阶段添加容器
在创建基础svg后,添加带class的容器:// append the svg object to the body of the page let svg = d3.select("#my_dataviz") .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform",`translate(${margin.left},${margin.top})`); // 提前创建坐标轴和柱状图容器 const xAxisGroup = svg.append("g") .attr("class", "x-axis") .attr("transform", `translate(0, ${height})`); const yAxisGroup = svg.append("g") .attr("class", "y-axis"); const barsGroup = svg.append("g") .attr("class", "bars-group");修改update函数复用容器
移除原代码中svg.append("g")创建新容器的逻辑,改为选择已有的容器更新,并在更新柱状图前清理旧的group:function update(test_no) { d3.csv(`bardisect_${test_no}.csv`).then(function(data) { let subgroups = data.columns.slice(1); let groups = data.map(d => d.groups); // 更新X轴 let x = d3.scaleBand() .domain(groups) .range([0, width]) .padding([0.2]); xAxisGroup.call(d3.axisBottom(x).tickSize(0)); // 更新Y轴 let y = d3.scaleLinear() .domain([0, 100]) .range([height, 0]); yAxisGroup.call(d3.axisLeft(y)); let xSubgroup = d3.scaleBand() .domain(subgroups) .range([0, x.bandwidth()]) .padding([0.05]); let color = d3.scaleOrdinal() .domain(subgroups) .range(['green','red','grey']); // 清理旧的柱状图组 barsGroup.selectAll("g").remove(); // 渲染新的柱状图 barsGroup.selectAll("g") .data(data) .join(enter => { return enter.append("g") .attr("transform", d => `translate(${x(d.groups)}, 0)`); }) .selectAll("rect") .data(d => subgroups.map(key => ({key: key, value: d[key]}))) .join( enter => { enter.append("rect") .attr('fill', 'white') .attr("x", d => xSubgroup(d.key)) .attr("y", d => y(d.value)) .attr("width", xSubgroup.bandwidth()) .attr('height', 0) .transition() .duration(1000) .attr("height", d => height - y(d.value)) .attr("fill", d => color(d.key)); }, update => { update.transition() .duration(1000) .attr("fill", d => color(d.key)) .attr("x", d => xSubgroup(d.key)) .attr("width", xSubgroup.bandwidth()) .attr("height", d => height - y(d.value)) .attr("y", d => y(d.value)); }, exit => { exit.transition() .duration(1000) .attr('height', 0) .remove(); } ); }); }
方案二:每次更新前清除旧元素(简单直接)
如果不想提前创建容器,可在update函数开头清除所有旧的坐标轴和柱状图元素:
function update(test_no) { // 清除所有旧元素 svg.selectAll(".x-axis, .y-axis, .bars-group").remove(); d3.csv(`bardisect_${test_no}.csv`).then(function(data) { let subgroups = data.columns.slice(1); let groups = data.map(d => d.groups); // 添加X轴并标记class let x = d3.scaleBand() .domain(groups) .range([0, width]) .padding([0.2]); svg.append("g") .attr("class", "x-axis") .attr("transform", `translate(0, ${height})`) .call(d3.axisBottom(x).tickSize(0)); // 添加Y轴并标记class let y = d3.scaleLinear() .domain([0, 100]) .range([height, 0]); svg.append("g") .attr("class", "y-axis") .call(d3.axisLeft(y)); let xSubgroup = d3.scaleBand() .domain(subgroups) .range([0, x.bandwidth()]) .padding([0.05]); let color = d3.scaleOrdinal() .domain(subgroups) .range(['green','red','grey']); // 添加柱状图容器并标记class svg.append("g") .attr("class", "bars-group") .selectAll("g") .data(data) .join(enter => { return enter.append("g") .attr("transform", d => `translate(${x(d.groups)}, 0)`); }) .selectAll("rect") .data(d => subgroups.map(key => ({key: key, value: d[key]}))) .join( enter => { enter.append("rect") .attr('fill', 'white') .attr("x", d => xSubgroup(d.key)) .attr("y", d => y(d.value)) .attr("width", xSubgroup.bandwidth()) .attr('height', 0) .transition() .duration(1000) .attr("height", d => height - y(d.value)) .attr("fill", d => color(d.key)); }, update => { update.transition() .duration(1000) .attr("fill", d => color(d.key)) .attr("x", d => xSubgroup(d.key)) .attr("width", xSubgroup.bandwidth()) .attr("height", d => height - y(d.value)) .attr("y", d => y(d.value)); }, exit => { exit.transition() .duration(1000) .attr('height', 0) .remove(); } ); }); }
关键注意点
- 避免重复调用
svg.append("g")创建新容器,优先复用已有元素 - 使用class选择器精准定位需要更新/删除的元素,防止误删基础svg容器
join方法仅处理绑定数据的元素,外层未绑定数据的容器需要手动清理或复用
内容的提问来源于stack exchange,提问作者Alasdair Jordan
相关产品推荐
相关产品推荐

