如何消除D3.js分组柱状图中缺失数据导致的柱间空白
解决D3.js分组柱状图缺失数据导致的空白问题
问题核心是当前代码为所有年龄组(即使某州无对应数据)都预留了位置,导致空白。要消除空白,需要让每个州只渲染存在数据的柱子,并动态分配组内的位置和宽度。
修改步骤
1. 过滤无效数据项
在绑定柱子数据时,筛掉每个州中值为undefined的年龄组,避免生成空柱子:
.data(function(d) { return keys.map(function(key) { return {key: key, value: d[key]}; }) .filter(item => item.value !== undefined); })
2. 为每个州创建独立子比例尺
不再使用全局的x1比例尺,而是为每个州单独生成子比例尺,基于该州的有效年龄组计算位置和带宽:
g.append("g") .selectAll("g") .data(data) .enter().append("g") .attr("transform", function(d) { return "translate(" + x0(d.State) + ",0)"; }) .each(function(d) { // 获取当前州的有效年龄组 const validKeys = keys.filter(key => d[key] !== undefined); // 创建专属子比例尺 const x1 = d3.scaleBand() .padding(0.05) .domain(validKeys) .rangeRound([0, x0.bandwidth()]); // 将比例尺绑定到当前组元素 d3.select(this).datum(Object.assign({}, d, {x1: x1})); })
3. 动态计算柱子位置与宽度
绘制柱子时,从当前组元素获取绑定的子比例尺,计算每个柱子的x坐标和宽度:
.selectAll("rect") .data(function(d) { return keys.map(function(key) { return {key: key, value: d[key]}; }) .filter(item => item.value !== undefined); }) .enter().append("rect") .attr("x", function(d) { const x1 = d3.select(this.parentNode).datum().x1; return x1(d.key); }) .attr("width", function() { const x1 = d3.select(this.parentNode).datum().x1; return x1.bandwidth(); })
修改后的完整代码
<!DOCTYPE html> <style> .axis .domain { display: none; } </style> <svg width="960" height="500"></svg> <script src="https://d3js.org/d3.v4.min.js"></script> <script> var svg = d3.select("svg"), margin = {top: 20, right: 20, bottom: 30, left: 40}, width = +svg.attr("width") - margin.left - margin.right, height = +svg.attr("height") - margin.top - margin.bottom, g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"); var x0 = d3.scaleBand() .rangeRound([0, width]) .paddingInner(0.1); var y = d3.scaleLinear() .rangeRound([height, 0]); var z = d3.scaleOrdinal() .range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]); var data = [ { "State": "CA", "Under 5 Years": 2704659, "5 to 13 Years": 4499890, "14 to 17 Years": 2159981, "18 to 24 Years": 3853788, "25 to 44 Years": 10604510, "45 to 64 Years": 8819342, "65 Years and Over": 4114496 }, { "State": "TX", "Under 5 Years": 2027307, "5 to 13 Years": 3277946, "45 to 64 Years": 5656528, "65 Years and Over": 2472223 }, { "State": "NY", "Under 5 Years": 1208495, "5 to 13 Years": 2141490, "25 to 44 Years": 5355235, "45 to 64 Years": 5120254, "65 Years and Over": 2607672 }, { "State": "FL", "Under 5 Years": 1140516, "5 to 13 Years": 1938695, "14 to 17 Years": 925060, "18 to 24 Years": 1607297, "25 to 44 Years": 4782119, "45 to 64 Years": 4746856, "65 Years and Over": 3187797 }, { "State": "IL", "Under 5 Years": 894368, "5 to 13 Years": 1558919, "14 to 17 Years": 725973, "18 to 24 Years": 1311479, "25 to 44 Years": 3596343, "45 to 64 Years": 3239173, "65 Years and Over": 1575308 }, { "State": "PA", "Under 5 Years": 737462, "5 to 13 Years": 1345341, "14 to 17 Years": 679201, "18 to 24 Years": 1203944, "25 to 44 Years": 3157759, "45 to 64 Years": 3414001, "65 Years and Over": 1910571 } ]; var keys = Object.keys(data[0]); keys = keys.slice(1); x0.domain(data.map(function(d) { return d.State; })); y.domain([0, d3.max(data, function(d) { return d3.max(keys, function(key) { return d[key]; }); })]).nice(); g.append("g") .selectAll("g") .data(data) .enter().append("g") .attr("transform", function(d) { return "translate(" + x0(d.State) + ",0)"; }) .each(function(d) { const validKeys = keys.filter(key => d[key] !== undefined); const x1 = d3.scaleBand() .padding(0.05) .domain(validKeys) .rangeRound([0, x0.bandwidth()]); d3.select(this).datum(Object.assign({}, d, {x1: x1})); }) .selectAll("rect") .data(function(d) { return keys.map(function(key) { return {key: key, value: d[key]}; }) .filter(item => item.value !== undefined); }) .enter().append("rect") .attr("x", function(d) { const x1 = d3.select(this.parentNode).datum().x1; return x1(d.key); }) .attr("y", function(d) { return y(d.value); }) .attr("width", function() { const x1 = d3.select(this.parentNode).datum().x1; return x1.bandwidth(); }) .attr("height", function(d) { return height - y(d.value); }) .attr("fill", function(d) { return z(d.key); }); g.append("g") .attr("class", "axis") .attr("transform", "translate(0," + height + ")") .call(d3.axisBottom(x0)); g.append("g") .attr("class", "axis") .call(d3.axisLeft(y).ticks(null, "s")) .append("text") .attr("x", 2) .attr("y", y(y.ticks().pop()) + 0.5) .attr("dy", "0.32em") .attr("fill", "#000") .attr("font-weight", "bold") .attr("text-anchor", "start") .text("Population"); var legend = g.append("g") .attr("font-family", "sans-serif") .attr("font-size", 10) .attr("text-anchor", "end") .selectAll("g") .data(keys.slice().reverse()) .enter().append("g") .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; }); legend.append("rect") .attr("x", width - 19) .attr("width", 19) .attr("height", 19) .attr("fill", z); legend.append("text") .attr("x", width - 24) .attr("y", 9.5) .attr("dy", "0.32em") .text(function(d) { return d; }); </script> </html>
关键说明
- 每个州的柱子组会根据自身有效数据动态计算布局,不存在数据的年龄组不会预留位置。
- 子比例尺确保每个州内的柱子均匀填充组内宽度,消除空白间隙。
内容的提问来源于stack exchange,提问作者Shijo C J
相关产品推荐
相关产品推荐

