Search code examples
javascriptchartsgoogle-visualization

How to insert points on top of bar charts using Google Charts?


Given the illustration

enter image description here

I was able to create the chart (without the points) using the following code:

<html>
  <head>
      <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
    <script type="text/javascript">
      google.charts.load('current', {'packages':['bar']});
      google.charts.setOnLoadCallback(drawStuff);

      var chart;

      function drawStuff() {
        var data = new google.visualization.arrayToDataTable([
          ['Y', 'Series 1','Series 2'],
          ["Element A", 44,11],
          ["Element B", 31,11],
          ["Element C", 12,11],
        ]);

        var options = {
          bars: 'horizontal', // Required for Material Bar Charts.
          axes: {
            x: {
              0: { side: 'bottom', label: 'X'} // Top x-axis.
            }
          }
        };

        var div = document.getElementById('top_x_div');
        var link = document.getElementById('url');
        chart = new google.charts.Bar(div);
        chart.draw(data, options);
        //console.log(chart.getImageURI());
      };
    </script>
  </head>
  <body>
    <div id="top_x_div" style="width: 900px; height: 500px;"></div>
  </body>
</html>

How to insert points on top of bar charts using Google Charts?


Solution

  • there were a couple ways I thought of, to accomplish...

    1. Use a Combo chart
    2. Add the points manually, on the chart's 'ready' event

    however, neither option can be achieved using a material chart.

    google.charts.Bar
    

    first, material charts do not support Combo charts.
    and, material charts do not have the methods necessary to add the points manually.

    not to mention, there are also several options that material charts do not support.
    see --> Tracking Issue for Material Chart Feature Parity #2143

    so we must use a classic chart...

    google.visualization.BarChart
    

    there is an option we can use to give the classic chart a similar look and feel as material charts...

    theme: 'material'
    

    now the solution...

    when attempting to use a Combo chart,
    the points align to the y-axis, in between the two bars.
    there is not an option that will allow the points to align on the bars.
    so we must add the points manually, on the chart's 'ready' event...

    there are few chart methods we need...

    getChartLayoutInterface() - Returns an object containing information about the onscreen placement of the chart and its elements.

    getBoundingBox(id) - Returns an object containing the left, top, width, and height of chart element id.

    getXLocation(position, optional_axis_index) - Returns the screen x-coordinate of position relative to the chart's container.

    once the chart's 'ready' event fires, we can get the layout interface using --> getChartLayoutInterface()

    the other two methods mentioned above are methods of the layout interface.

    we use getBoundingBox(id) to get the coordinates of the bar, on which the point will be placed.
    this will give us the Y coordinate, and the height of the point.
    the id of the bar is determined by the data row and series column.

    bar#C#R  // where C is the series column index, and R is the row index
    

    we are providing the X coordinate,
    getXLocation will provide the location on the chart, needed to place the point on the x-axis, at our predetermined location.

    // x coordinates of points to add
    var points = [
      [36, 8],
      [28, 8],
      [10, 8],
    ];
    
    // loop data rows and columns
    for (var r = 0; r < data.getNumberOfRows(); r++) {
      for (var c = 1; c < data.getNumberOfColumns(); c++) {
        // calculate position of the point
        var barBounds = chartLayout.getBoundingBox('bar#' + (c - 1) + '#' + r);
        var xCoord = chartLayout.getXLocation(points[r][c - 1]);
        var height = barBounds.height / 4;
        var top = barBounds.top + (height * 2);
    
        // create and add the point to the chart
        var point = document.createElementNS(svgNS, 'circle');
        point.setAttribute('cx', xCoord);
        point.setAttribute('cy', top);
        point.setAttribute('r', height);
        point.setAttribute('stroke', '#ffffff');
        point.setAttribute('stroke-width', height);
        point.setAttribute('fill', 'transparent');
        svg.appendChild(point);
      }
    }
    

    see following working snippet...

    var chart;
    
    google.charts.load('current', {
      packages: ['corechart']
    }).then(function drawStuff() {
      var data = new google.visualization.arrayToDataTable([
        ['Y', 'Series 1', 'Series 2'],
        ['Element A', 44, 11],
        ['Element B', 31, 11],
        ['Element C', 12, 11],
      ]);
    
      // x coordinates of points to add
      var points = [
        [36, 8],
        [28, 8],
        [10, 8],
      ];
    
      var options = {
        chartArea: {
          left: 128,
          top: 24,
          right: 128,
          bottom: 72,
          height: '100%',
          width: '100%'
        },
        hAxis: {
          title: 'X'
        },
        height: '100%',
        theme: 'material',
        vAxis: {
          title: data.getColumnLabel(0)
        },
        width: '100%'
      };
    
      var div = document.getElementById('top_x_div');
      //var link = document.getElementById('url');
    
      chart = new google.visualization.BarChart(div);
    
      google.visualization.events.addListener(chart, 'ready', function () {
        // get chart layour interface and svg
        var chartLayout = chart.getChartLayoutInterface();
        var svg = div.getElementsByTagName('svg')[0];
        var svgNS = svg.namespaceURI;
    
        // loop data rows and columns
        for (var r = 0; r < data.getNumberOfRows(); r++) {
          for (var c = 1; c < data.getNumberOfColumns(); c++) {
            // calculate position of the point
            var barBounds = chartLayout.getBoundingBox('bar#' + (c - 1) + '#' + r);
            var xCoord = chartLayout.getXLocation(points[r][c - 1]);
            var height = barBounds.height / 4;
            var top = barBounds.top + (height * 2);
    
            // create and add the point to the chart
            var point = document.createElementNS(svgNS, 'circle');
            point.setAttribute('cx', xCoord);
            point.setAttribute('cy', top);
            point.setAttribute('r', height);
            point.setAttribute('stroke', '#ffffff');
            point.setAttribute('stroke-width', height);
            point.setAttribute('fill', 'transparent');
            svg.appendChild(point);
          }
        }
      });
    
      chart.draw(data, options);
      window.addEventListener('resize', function () {
        chart.draw(data, options);
      });
    });
    #top_x_div {
      height: 400px;
    }
    <script src="https://www.gstatic.com/charts/loader.js"></script>
    <div id="top_x_div"></div>