You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

D3.js柱状图:动态数据下保持柱宽恒定并添加滚动条

实现动态数据下的滚动柱状图(保持柱宽恒定)

嘿,这个需求很实用!要实现数据量少的时候柱宽不变、数据量多的时候自动出现横向滚动条,我们只需要把图表放进一个可滚动的容器,再根据数据量动态计算图表的实际宽度就行。下面是具体的修改步骤和完整代码:

核心思路

  • 给图表套一个固定宽度、横向可滚动的容器,超出容器宽度的部分会自动出现滚动条
  • 预先定义固定的柱宽和柱子间距,保证数据量少的时候柱宽不会被拉伸/压缩
  • 根据数据条数计算图表需要的最小宽度,对比容器宽度后决定最终的SVG宽度:
    • 如果计算出的最小宽度 ≤ 容器宽度:SVG宽度等于容器宽度,柱子均匀分布
    • 如果计算出的最小宽度 > 容器宽度:SVG宽度等于最小宽度,容器出现滚动条

修改后的完整代码

<!-- 先添加滚动容器 -->
<div id="chart-container" style="width: 960px; overflow-x: auto; padding-bottom: 10px;"></div>

<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
// Setup svg using Bostock's margin convention 
var margin = {top: 20, right: 160, bottom: 35, left: 30};
// 定义固定的柱宽和间距
var barWidth = 40;
var barGap = 8;
// 容器固定宽度
var containerWidth = 960;

// 动态计算SVG需要的宽度
var data = [
  { year: "2006", redDelicious: "10", mcintosh: "15", oranges: "9", pears: "6" },
  { year: "2007", redDelicious: "12", mcintosh: "18", oranges: "9", pears: "4" },
  { year: "2008", redDelicious: "05", mcintosh: "20", oranges: "8", pears: "2" },
  { year: "2009", redDelicious: "01", mcintosh: "15", oranges: "5", pears: "4" },
  { year: "2010", redDelicious: "02", mcintosh: "10", oranges: "4", pears: "2" },
  { year: "2011", redDelicious: "03", mcintosh: "12", oranges: "6", pears: "3" },
  { year: "2012", redDelicious: "04", mcintosh: "15", oranges: "8", pears: "1" },
  { year: "2013", redDelicious: "06", mcintosh: "11", oranges: "9", pears: "4" },
  { year: "2014", redDelicious: "10", mcintosh: "13", oranges: "9", pears: "5" },
  { year: "2015", redDelicious: "16", mcintosh: "19", oranges: "6", pears: "9" },
  { year: "2016", redDelicious: "19", mcintosh: "17", oranges: "5", pears: "7" },
  { year: "2017", redDelicious: "04", mcintosh: "15", oranges: "8", pears: "1" },
  { year: "2018", redDelicious: "06", mcintosh: "11", oranges: "9", pears: "4" },
  // 可以复制更多数据测试滚动效果
];
// 计算图表内容的最小宽度(柱子+间距的总宽度)
var contentMinWidth = data.length * (barWidth + barGap) + 20; // 左右各留10px边距
// 最终SVG的宽度:取容器可用宽度和最小宽度的较大值
var svgWidth = Math.max(containerWidth - margin.left - margin.right, contentMinWidth);
var height = 500 - margin.top - margin.bottom;

// 把SVG放到滚动容器里,而不是body
var svg = d3.select("#chart-container")
  .append("svg")
  .attr("width", svgWidth + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

var parse = d3.time.format("%Y").parse;

// Transpose the data into layers 
var dataset = d3.layout.stack()(["redDelicious", "mcintosh", "oranges", "pears"].map(function(fruit) {
  return data.map(function(d) {
    return {x: parse(d.year), y: +d[fruit]};
  });
}));

// Set x, y and colors
// 修改x轴scale:用rangeRoundBands适配自定义的柱子间距
var x = d3.scale.ordinal()
  .domain(dataset[0].map(function(d) { return d.x; }))
  .rangeRoundBands([10, svgWidth-10], 0, barGap/(barWidth+barGap)); // 自定义间距比例

var y = d3.scale.linear()
  .domain([0, d3.max(dataset, function(d) { return d3.max(d, function(d) { return d.y0 + d.y; }); })])
  .range([height, 0]);

var colors = ["#b33040", "#d25c4d", "#f2b447", "#d9d574"];

// Define and draw axes
var yAxis = d3.svg.axis()
  .scale(y)
  .orient("left")
  .ticks(5)
  .tickSize(-svgWidth, 0, 0) // 适配动态SVG宽度,保证刻度线贯穿图表
  .tickFormat( function(d) { return d } );

var xAxis = d3.svg.axis()
  .scale(x)
  .orient("bottom")
  .tickFormat(d3.time.format("%Y"));

svg.append("g")
  .attr("class", "y axis")
  .call(yAxis);

svg.append("g")
  .attr("class", "x axis")
  .attr("transform", "translate(0," + height + ")")
  .call(xAxis);

// Create groups for each series, rects for each segment
var groups = svg.selectAll("g.cost")
  .data(dataset)
  .enter().append("g")
  .attr("class", "cost")
  .style("fill", function(d, i) { return colors[i]; });

var rect = groups.selectAll("rect")
  .data(function(d) { return d; })
  .enter()
  .append("rect")
  .attr("x", function(d) { return x(d.x); })
  .attr("y", function(d) { return y(d.y0 + d.y); })
  .attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); })
  .attr("width", x.rangeBand()) // 自动匹配固定柱宽
  .on("mouseover", function() { tooltip.style("display", null); })
  .on("mouseout", function() { tooltip.style("display", "none"); })
  .on("mousemove", function(d) {
    var xPosition = d3.mouse(this)[0] - 15;
    var yPosition = d3.mouse(this)[1] - 25;
    tooltip.attr("transform", "translate(" + xPosition + "," + yPosition + ")");
    tooltip.select("text").text(d.y);
  });

// Draw legend
var legend = svg.selectAll(".legend")
  .data(colors)
  .enter().append("g")
  .attr("class", "legend")
  .attr("transform", function(d, i) { return "translate(30," + i * 19 + ")"; });

legend.append("rect")
  .attr("x", svgWidth - 18) // 适配动态SVG宽度,保证图例在右侧
  .attr("width", 18)
  .attr("height", 18)
  .style("fill", function(d, i) {return colors.slice().reverse()[i];});

legend.append("text")
  .attr("x", svgWidth + 5) // 适配动态SVG宽度
  .attr("y", 9)
  .attr("dy", ".35em")
  .style("text-anchor", "start")
  .text(function(d, i) {
    switch (i) {
      case 0: return "Anjou pears";
      case 1: return "Naval oranges";
      case 2: return "McIntosh apples";
      case 3: return "Red Delicious apples";
    }
  });

// Prep the tooltip bits, initial display is hidden
var tooltip = svg.append("g")
  .attr("class", "tooltip")
  .style("display", "none");

tooltip.append("rect")
  .attr("width", 30)
  .attr("height", 20)
  .attr("fill", "white")
  .style("opacity", 0.5);

tooltip.append("text")
  .attr("x", 15)
  .attr("dy", "1.2em")
  .style("text-anchor", "middle")
  .attr("font-size", "12px")
  .attr("font-weight", "bold");
</script>

关键修改点说明

  • 添加滚动容器:新增#chart-containerdiv,设置固定宽度和overflow-x: auto,实现横向滚动
  • 动态计算SVG宽度:根据数据条数和固定的柱宽/间距,计算图表需要的最小宽度,再和容器宽度对比确定最终SVG宽度
  • 调整x轴scale:用rangeRoundBands适配自定义的柱子间距,保证柱宽恒定
  • 适配轴和图例位置:把原来硬编码的width换成动态计算的svgWidth,确保轴和图例在任何宽度下都显示正确

这样修改后,当数据量少的时候,图表会撑满容器宽度,柱宽保持你定义的40px;当数据量增多,总宽度超过容器宽度时,容器会自动出现横向滚动条,柱子宽度依然不变,完美解决你的需求!

内容的提问来源于stack exchange,提问作者Madhuri

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:22:31