如何基于D3.js将现有垂直柱状图转换为水平柱状图
将D3.js垂直分组柱状图转为水平分组柱状图
我已经帮你把垂直柱状图改成了水平版本,直接替换你的代码就能运行。下面是完整的修改后代码,之后我会解释关键的调整点:
修改后的完整代码
JavaScript 部分
$(document).ready(function() { render_chart(); }); function render_chart() { var stack = d3.stack; var dataset = { "categories": ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'], "series": ["Group1", "Group2", "Group3"], "colors": ["#3498db", "#e74c3c", "#2ecc71"], "layers": [ [{ "y": 1, "y0": 20, "month": "Monday" }, { "y": 2, "y0": 18, "month": "Tuesday" }, { "y": 5, "y0": 18, "month": "Wednesday" }, { "y": 10, "y0": 20, "month": "Thursday" }, { "y": 14, "y0": 23, "month": "Friday" }, { "y": 18, "y0": 24, "month": "Saturday" }, { "y": 20, "y0": 24, "month": "Sunday" }], [{ "y": 12, "y0": 24, "month": "Monday" }, { "y": 14, "y0": 24, "month": "Tuesday" }, { "y": 13, "y0": 23, "month": "Wednesday" }, { "y": 16, "y0": 21, "month": "Thursday" }, { "y": 18, "y0": 23, "month": "Friday" }, { "y": 19, "y0": 22, "month": "Saturday" }, { "y": 20, "y0": 22, "month": "Sunday" }], [{ "y": 8, "y0": 24, "month": "Monday" }, { "y": 14, "y0": 21, "month": "Tuesday" }, { "y": 12, "y0": 19, "month": "Wednesday" }, { "y": 15, "y0": 23, "month": "Thursday" }, { "y": 18, "y0": 21, "month": "Friday" }, { "y": 16, "y0": 23, "month": "Saturday" }, { "y": 17, "y0": 24, "month": "Sunday" }] ] }; n = dataset["series"].length; m = dataset["layers"].length; yGroupMax = d3.max(dataset["layers"], function(layer) { return d3.max(layer, function(d) { return d.y0; }); }); yGroupMin = d3.min(dataset["layers"], function(layer) { return d3.min(layer, function(d) { return d.y; }); }); // 调整margin,适配水平布局(增加底部空间放图例,左侧保持足够宽度) var margin = { top: 50, right: 50, bottom: 80, left: 120 }, width = 900 - margin.left - margin.right, height = 600 - margin.top - margin.bottom; // 增加高度适配垂直排列的日期 // 交换比例尺:y轴用band对应日期,x轴用线性对应数值 var yScale = d3.scaleBand() .domain(dataset["categories"]) .rangeRound([0, height]) // y轴从上到下排列日期 .padding(.08); var xScale = d3.scaleLinear() .domain([yGroupMin, yGroupMax]) .range([0, width]); // x轴从左到右数值增大 // 调整坐标轴方向 var xAxis = d3.axisBottom(xScale) .tickSize(7) .tickPadding(6); var yAxis = d3.axisLeft(yScale).tickSize(0); var svg = d3.select("#groupchart").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 + ")"); var layer = svg.selectAll(".layer") .data(dataset["layers"]) .enter().append("g") .attr("class", "layer"); var rect = layer.selectAll("rect") .data(function(d, i) { d.map(function(b) { b.colorIndex = i; return b; }); return d; }) .enter() .append("rect") .transition() .duration(500) .delay(function(d, i) { return i * 10; }) // 交换x/y属性:y对应日期分组,height对应每组柱子高度 .attr("y", function(d, i, j) { var k = Array.prototype.indexOf.call(j[i].parentNode.parentNode.childNodes, j[i].parentNode); return yScale(d.month) + yScale.bandwidth() / n * k; }) .attr("height", yScale.bandwidth() / n) .transition() // x对应数值起点,width对应数值范围长度 .attr("x", function(d) { return xScale(d.y); }) .attr("width", function(d) { return xScale(d.y0) - xScale(d.y); }) .attr("class", "bar") .style("fill", function(d) { return dataset["colors"][d.colorIndex]; }); // 绘制x轴(底部) svg.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") .call(xAxis); // 绘制y轴(左侧) svg.append("g") .attr("class", "y axis") .call(yAxis); // 标题调整位置 svg.append("text") .attr("x", width / 2) .attr("y", -20) .attr("text-anchor", "middle") .text("Horizontal Grouped Bar Chart Test"); // 图例调整到底部 var legend = svg.append("g") .attr("class", "legend") .attr("transform", "translate(" + (width/2 - 180) + "," + (height + 30) + ")"); // 居中放置 legend.selectAll('rect') .data(dataset["colors"]) .enter() .append("rect") .attr("x", function(d, i) { return i * 120; }) .attr("y", 0) .attr("width", 10) .attr("height", 10) .style("fill", function(d) { return d; }); legend.selectAll('text') .data(dataset["series"]) .enter() .append("text") .attr("x", function(d, i) { return i * 120 + 12; }) .attr("y", 9) .text(function(d) { return d; }); // 调整tooltip位置逻辑 var tooltip = d3.select("body") .append('div') .attr('class', 'tooltip'); tooltip.append('div') .attr('class', 'month'); tooltip.append('div') .attr('class', 'tempRange'); svg.selectAll("rect") .on('mouseover', function(d) { if (!d.month) return null; tooltip.select('.month').html("<b>" + d.month + "</b>"); tooltip.select('.tempRange').html(d.y + ":00 Hours to " + d.y0 + ":00 Hours"); tooltip.style('display', 'block'); tooltip.style('opacity', 1); }) .on('mousemove', function(d) { if (!d.month) return null; tooltip.style('top', (d3.event.pageY - 30) + 'px') .style('left', (d3.event.pageX + 10) + 'px'); }) .on('mouseout', function() { tooltip.style('display', 'none'); tooltip.style('opacity', 0); }); }
CSS 部分
.axis path, .axis line { fill: none; font: 10px sans-serif; stroke: #000; shape-rendering: crispEdges; } .bar { opacity: 0.8; transition: opacity 0.2s; } .bar:hover { opacity: 1; } .tooltip { position: absolute; background-color: rgba(0, 0, 0, 0.8); color: #fff; padding: 8px 12px; border-radius: 4px; font-size: 12px; pointer-events: none; }
关键修改点说明
我主要做了以下核心调整来实现水平布局:
- 比例尺互换:把原来对应日期的
xScale(band类型)改成yScale,对应数值的yScale(线性)改成xScale,并调整它们的range适配水平方向 - 矩形属性交换:将柱子的
x/y属性和width/height属性互换,计算逻辑也对应调整——y对应日期分组位置,height对应每组柱子高度,x对应数值起点,width对应数值范围长度 - 坐标轴调整:保持x轴在底部、y轴在左侧,但更新了坐标轴的比例尺关联,优化了y轴的tick样式
- 布局适配:调整了margin值(增加底部空间放图例)、svg高度,重新定位了标题和图例的位置,让整体布局更协调
- Tooltip优化:修改了鼠标移动时tooltip的定位逻辑,改用
pageX/pageY避免布局偏移问题,同时调整了hover时的透明度效果
内容的提问来源于stack exchange,提问作者Rajkishan Swami
相关产品推荐
相关产品推荐

