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
相关产品推荐
相关产品推荐

