Search code examples
chartsgoogle-visualizationpolygonscatterconvex-hull

How to draw a custom polygon over a Scatter Series google chart?


I have a Scatter Series with a set of points, like the one shown here. https://developers.google.com/chart/interactive/docs/gallery/scatterchart

The points are grouped and each group is shown in different color. I would like to draw a polygon around each group (convex hull). Looks like there is not a straightforward way to add polygons each with n boundary-points to the chart.


Solution

  • if you have an algorithm to find the boundary points,
    you can use a ComboChart to draw both the scatter and line series...
    use option seriesType to set the default type
    use option series to customize the type for a particular series

    in the following working snippet,
    the algorithm used was pulled from --> Convex Hull | Set 1 (Jarvis’s Algorithm or Wrapping)
    (converted from the Java version)

    google.charts.load('current', {
      packages: ['corechart']
    }).then(function () {
      var groupA = [
        [0,3],[2,3],[1,1],[2,1],[3,0],[0,0],[3,3],[2,2]
      ];
    
      var groupB = [
        [11,11],[12,12],[12,10],[12,14],[13,13],[14,12],[15,12],[16,12]
      ];
    
      var data = new google.visualization.DataTable();
      data.addColumn('number', 'x');
      data.addColumn('number', 'y');
      data.addRows(groupA);
      data.addRows(groupB);
    
      addGroup('A', data, groupA)
      addGroup('B', data, groupB)
    
      var options = {
        chartArea: {
          bottom: 48,
          height: '100%',
          left: 36,
          right: 24,
          top: 36,
          width: '100%'
        },
        height: '100%',
        seriesType: 'line',
        series: {
          0: {
            type: 'scatter'
          }
        },
        width: '100%'
      };
    
      var chart = new google.visualization.ComboChart(document.getElementById('chart_div'));
    
      drawChart();
      window.addEventListener('resize', drawChart, false);
    
      function drawChart() {
        chart.draw(data, options);
      }
    
      function addGroup(group, dataTable, points) {
        var polygon = convexHull(points);
        var colIndex = dataTable.addColumn('number', group);
        for (var i = 0; i < polygon.length; i++) {
          var rowIndex = dataTable.addRow();
          dataTable.setValue(rowIndex, 0, polygon[i][0]);
          dataTable.setValue(rowIndex, colIndex, polygon[i][1]);
        }
      }
    
      function orientation(p, q, r) {
        var val = (q[1] - p[1]) * (r[0] - q[0]) -
                  (q[0] - p[0]) * (r[1] - q[1]);
    
        if (val == 0) {
          return 0;  // collinear
        } else if (val > 0) {
          return 1;  // clock wise
        } else {
          return 2;  // counterclock wise
        }
      }
    
      function convexHull(points) {
        // must be at least 3 rows
        if (points.length < 3) {
          return;
        }
    
        // init
        var l = 0;
        var p = l;
        var q;
        var hull = [];
    
        // find leftmost point
        for (var i = 1; i < points.length; i++) {
          if (points[i][0] < points[l][0]) {
            l = i;
          }
        }
    
        // move counterclockwise until start is reached
        do {
          // add current point to result
          hull.push(points[p]);
    
          // check orientation (p, x, q) of each point
          q = (p + 1) % points.length;
          for (var i = 0; i < points.length; i++) {
            if (orientation(points[p], points[i], points[q]) === 2) {
              q = i;
            }
          }
    
          // set p as q for next iteration
          p = q;
        } while (p !== l);
    
        // add back first hull point to complete line
        hull.push(hull[0]);
    
        // set return value
        return hull;
      }
    });
    html, body, #chart_div {
      height: 100%;
    }
    <script src="https://www.gstatic.com/charts/loader.js"></script>
    <div id="chart_div"></div>