Google Visualization双Y轴堆叠组合图柱形重叠问题求助
双Y轴堆叠柱形图并排显示解决方案
问题描述
使用Google Charts Combo Chart实现双Y轴堆叠柱形时,两组堆叠柱(分别绑定到不同Y轴)会重叠在一起,导致下方的柱形被遮挡。需要调整实现逻辑,让两组堆叠柱并排显示。
初始实现代码
JavaScript
google.charts.load('current', {'packages':['corechart']}); google.charts.setOnLoadCallback(drawVisualization); function drawVisualization() { // 示例数据 var data = google.visualization.arrayToDataTable([ ['Month', 'Bolivia', 'Ecuador', 'Madagascar', 'Ethiopia', 'Average'], ['2004/05', 10, 15, 20, 25, 18], ['2005/06', 10, 15, 20, 25, 18], ['2006/07', 10, 15, 20, 25, 18], ['2007/08', 10, 15, 20, 25, 18], ['2008/09', 10, 15, 20, 25, 18] ]); var options = { title : 'Monthly Coffee Production by Country', vAxis: {title: 'Cups'}, hAxis: {title: 'Month'}, seriesType: 'bars', vAxis: { viewWindow: { max: 35, } }, series: { 0: {type: 'bar', targetAxisIndex: 0}, 1: {type: 'bar', targetAxisIndex: 0}, 2: {type: 'bar', targetAxisIndex: 1}, 3: {type: 'bar', targetAxisIndex: 1}, 4: {type: 'line',targetAxisIndex: 0}, }, isStacked: true }; var chart = new google.visualization.ComboChart(document.getElementById('chart_div')); chart.draw(data, options); }
HTML
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script> <div id="chart_div" style="width: 900px; height: 500px;"></div>
进展更新
UPDATE1
添加focusTarget: 'category'配置,优化提示框交互,鼠标悬停时显示当前分类下的所有数值。
UPDATE2
修改代码后初始绘制能显示所有柱形,但MutationObserver无法正常工作,无法在DOM变更后维持柱形的并排状态,代码如下:
JavaScript
google.charts.load('current', { 'packages': ['corechart'] }); google.charts.setOnLoadCallback(drawVisualization); function drawVisualization() { // 示例数据 var data = google.visualization.arrayToDataTable([ ['Month', 'Made', 'PlanToMake', 'Delivered', 'PlanToDeliver', 'Goal'], ['2004/03', 10, 0, 10, 0, 10], ['2005/04', 8, 2, 8, 2, 10], // 交叉月份 ['2006/05', 0, 10, 0, 10, 10], ['2007/06', 0, 10, 0, 10, 10], ['2008/07', 0, 10, 0, 10, 10], ]); var options = { vAxis: { title: 'Cups' }, hAxis: { title: 'Month' }, seriesType: 'bars', legend: { position: "top", }, hAxis: { slantedText: true, slantedTextAngle: 90 }, focusTarget: 'category', // 优化提示框显示 vAxis: { viewWindow: { max: 20, } }, series: { 0: { type: 'bar', targetAxisIndex: 0, color: "rgb(0, 86, 145)" }, 1: { type: 'bar', targetAxisIndex: 0, color: "rgb(252, 175, 23)" }, 2: { type: 'bar', targetAxisIndex: 1, color: "rgb(0, 103, 174)" }, 3: { type: 'bar', targetAxisIndex: 1, color: "rgb(252, 210, 28)" }, 4: { type: 'line', targetAxisIndes: 0, // 拼写错误 color: "rgb(127, 198, 231)" }, }, isStacked: true }; var chart = new google.visualization.ComboChart(document.getElementById('chart_div')); var chartLayout; var chartBounds; var legendColors = []; var saveCoords = {}; google.visualization.events.addListener(chart, 'ready', function() { // 初始化图表元素 chartLayout = chart.getChartLayoutInterface(); chartBounds = chartLayout.getChartAreaBoundingBox(); var bars = chart.getContainer().getElementsByTagName('rect'); // 提取图例颜色 Array.prototype.forEach.call(bars, function(bar) { var color = bar.getAttribute('fill'); var xCoord = parseFloat(bar.getAttribute('x')); var yCoord = parseFloat(bar.getAttribute('y')); if ((xCoord >= chartBounds.left) && (yCoord < chartBounds.top) && (color !== '#ffffff')) { legendColors.push(color); } }); // 调整柱形位置和宽度 Array.prototype.forEach.call(bars, function(bar, index) { var xCoord = parseFloat(bar.getAttribute('x')); var yCoord = parseFloat(bar.getAttribute('y')); if ((xCoord > chartBounds.left) && (yCoord > chartBounds.top)) { var color = bar.getAttribute('fill'); var width = parseFloat(bar.getAttribute('width')) / 2; bar.setAttribute('width', width); saveCoords[xCoord + yCoord] = { width: width, x: xCoord }; if (legendColors.indexOf(color) > 1) { bar.setAttribute('x', xCoord + width); saveCoords[xCoord + width + yCoord] = { width: width, x: xCoord + width, }; } } }); var observer = new MutationObserver(function() { // DOM变更后重置柱形 var bars = chart.getContainer().getElementsByTagName('rect'); Array.prototype.forEach.call(bars, function(bar, index) { var xCoord = parseFloat(bar.getAttribute('x')); var yCoord = parseFloat(bar.getAttribute('y')); if ((xCoord > chartBounds.left) && (yCoord > chartBounds.top)) { var color = bar.getAttribute('fill'); var width = parseFloat(bar.getAttribute("width")) / 2; if (saveCoords.hasOwnProperty(xCoord + width + yCoord)) { bar.setAttribute("width", saveCoords[xCoord + width + yCoord].width); bar.setAttribute("x", saveCoords[xCoord + width + yCoord].x); } } }); }); observer.observe(chart.getContainer(), { childList: true, subtree: true }); }); chart.draw(data, options); window.addEventListener('resize', function() { chart.draw(data, options); }); }
HTML
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script> <div id="chart_div" style="width: 900px; height: 500px;"></div>
修复方案
关键问题分析
series[4]中存在拼写错误:targetAxisIndes应为targetAxisIndex,导致系列配置异常saveCoords的存储逻辑依赖xCoord + yCoord作为键值,DOM变更后坐标会变化,无法匹配旧存储的键- MutationObserver回调逻辑中,判断条件不准确,无法正确找到需要调整的柱形
修复后的完整代码
google.charts.load('current', { 'packages': ['corechart'] }); google.charts.setOnLoadCallback(drawVisualization); function drawVisualization() { // 示例数据 var data = google.visualization.arrayToDataTable([ ['Month', 'Made', 'PlanToMake', 'Delivered', 'PlanToDeliver', 'Goal'], ['2004/03', 10, 0, 10, 0, 10], ['2005/04', 8, 2, 8, 2, 10], ['2006/05', 0, 10, 0, 10, 10], ['2007/06', 0, 10, 0, 10, 10], ['2008/07', 0, 10, 0, 10, 10], ]); var options = { vAxis: { title: 'Cups' }, hAxis: { title: 'Month', slantedText: true, slantedTextAngle: 90 }, seriesType: 'bars', legend: { position: "top" }, focusTarget: 'category', vAxis: { viewWindow: { max: 20 } }, series: { 0: { type: 'bar', targetAxisIndex: 0, color: "rgb(0, 86, 145)" }, 1: { type: 'bar', targetAxisIndex: 0, color: "rgb(252, 175, 23)" }, 2: { type: 'bar', targetAxisIndex: 1, color: "rgb(0, 103, 174)" }, 3: { type: 'bar', targetAxisIndex: 1, color: "rgb(252, 210, 28)" }, 4: { type: 'line', targetAxisIndex: 0, color: "rgb(127, 198, 231)" } // 修复拼写错误 }, isStacked: true }; var chart = new google.visualization.ComboChart(document.getElementById('chart_div')); var observer; function adjustBars() { var chartLayout = chart.getChartLayoutInterface(); var chartBounds = chartLayout.getChartAreaBoundingBox(); var bars = chart.getContainer().getElementsByTagName('rect'); var legendColors = []; // 提取图例颜色 Array.prototype.forEach.call(bars, function(bar) { var color = bar.getAttribute('fill'); var x = parseFloat(bar.getAttribute('x')); var y = parseFloat(bar.getAttribute('y')); if (x >= chartBounds.left && y < chartBounds.top && color !== '#ffffff') { legendColors.push(color); } }); // 调整柱形宽度和位置 Array.prototype.forEach.call(bars, function(bar) { var x = parseFloat(bar.getAttribute('x')); var y = parseFloat(bar.getAttribute('y')); if (x > chartBounds.left && y > chartBounds.top) { var color = bar.getAttribute('fill'); var originalWidth = parseFloat(bar.getAttribute('width')); // 仅在未调整过的情况下修改,避免重复调整 if (originalWidth !== originalWidth / 2) { var newWidth = originalWidth / 2; bar.setAttribute('width', newWidth); // 第二组系列(索引>1)的柱形向右偏移半个宽度 if (legendColors.indexOf(color) > 1) { bar.setAttribute('x', x + newWidth); } } } }); } google.visualization.events.addListener(chart, 'ready', function() { adjustBars(); // 初始化MutationObserver observer = new MutationObserver(function() { adjustBars(); }); observer.observe(chart.getContainer(), { childList: true, subtree: true }); }); // 窗口resize时重新绘制并调整 window.addEventListener('resize', function() { if (observer) { observer.disconnect(); } chart.draw(data, options); }); chart.draw(data, options); }
修复说明
- 修正了系列配置中的拼写错误,确保系列绑定到正确的Y轴
- 将柱形调整逻辑封装为
adjustBars函数,便于重复调用 - MutationObserver触发时直接调用
adjustBars,不再依赖旧坐标存储,避免键值不匹配的问题 - 增加判断逻辑,避免重复调整柱形宽度
- 窗口resize时先断开旧的Observer,重新绘制后再初始化新的Observer,避免内存泄漏和异常
内容的提问来源于stack exchange,提问作者cmill
相关产品推荐
相关产品推荐

