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

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>

修复方案

关键问题分析

  1. series[4]中存在拼写错误:targetAxisIndes应为targetAxisIndex,导致系列配置异常
  2. saveCoords的存储逻辑依赖xCoord + yCoord作为键值,DOM变更后坐标会变化,无法匹配旧存储的键
  3. 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);
}

修复说明

  1. 修正了系列配置中的拼写错误,确保系列绑定到正确的Y轴
  2. 将柱形调整逻辑封装为adjustBars函数,便于重复调用
  3. MutationObserver触发时直接调用adjustBars,不再依赖旧坐标存储,避免键值不匹配的问题
  4. 增加判断逻辑,避免重复调整柱形宽度
  5. 窗口resize时先断开旧的Observer,重新绘制后再初始化新的Observer,避免内存泄漏和异常

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 08:44:54