22

How can you find the centroid of a concave irregular polygon given its vertices in JavaScript?

I want to pass a set of x,y points to a JavaScript function and be given an x,y point.

var my_points = [{x:3,y:1},{x:5,y:8},{x:2,y:9}];

function get_polygon_centroid(points){
    // answer
}

var my_centroid = get_polygon_centroid(my_points);

The my_points variable is only supposed to represent the format of the points to be given, not to represent the specific count of points to be given.

The centroid returned will be a point somewhere inside the polygon.

The end goal would be to add a marker at the centroid of a polygon in a Google Maps V3 application.

Ian Mercer
  • 38,490
  • 8
  • 97
  • 133
iambriansreed
  • 21,935
  • 6
  • 63
  • 79
  • this article has javascript code: http://paulbourke.net/geometry/polyarea/ – Zevan Mar 13 '12 at 21:32
  • Have you considered `google.maps.LatLngBounds.getCenter()`? – Alex Korban Mar 13 '12 at 21:38
  • 1
    I found this code: http://jsfiddle.net/SXde5/ – Joe Mar 13 '12 at 21:40
  • @AlexKorban center != centroid - "concave irregular polygon" – iambriansreed Mar 13 '12 at 21:45
  • As Zevan suggested, there is a JavaScript centroid function at http://paulbourke.net/geometry/polyarea/javascript.txt. Not entirely sure that it would handle convex polygons, but the accompanying illustration suggests that it does. – Alex Korban Mar 13 '12 at 21:52
  • The java script functions referenced above do not work consistently. Some shapes it does a great job. Other very simple polygons it it's even inside the shape. – iambriansreed Mar 14 '12 at 20:38
  • Not sure if I understood that last comment, but the centroid of a concave polygon is not always contained in the polygon. – ellisbben Mar 26 '12 at 22:20
  • @iambriansreed The centroid of a concave polygon is not necessarily inside of that polygon. An easy example is a very long, thin L-shaped object; its centroid falls between the two limbs of the L. If you want something that is guaranteed to be inside the polygon, the centroid isn't what you are looking for. – ellisbben Apr 01 '12 at 21:50
  • @ellisbben what would be something that places the point inside the largest 'area' or similar of the polygon? – Asgaroth Apr 16 '14 at 22:07
  • the polyarea link provided by @Zevan is returning 404 I suppose it is probably the following http://paulbourke.net/geometry/polygonmesh/ which has a centroid function in it. the javascript is at http://paulbourke.net/geometry/polygonmesh/javascript.txt the site seems to have very poor performance so be prepared to wait – user254694 Apr 16 '20 at 11:22

6 Answers6

31

For the centroid of the 2D surface (which is likely to be what you need), The best is to start with a little bit of maths.

I adapted it here to your own notation :

function get_polygon_centroid(pts) {
   var first = pts[0], last = pts[pts.length-1];
   if (first.x != last.x || first.y != last.y) pts.push(first);
   var twicearea=0,
   x=0, y=0,
   nPts = pts.length,
   p1, p2, f;
   for ( var i=0, j=nPts-1 ; i<nPts ; j=i++ ) {
      p1 = pts[i]; p2 = pts[j];
      f = p1.x*p2.y - p2.x*p1.y;
      twicearea += f;          
      x += ( p1.x + p2.x ) * f;
      y += ( p1.y + p2.y ) * f;
   }
   f = twicearea * 3;
   return { x:x/f, y:y/f };
}
r34
  • 320
  • 2
  • 12
Myobis
  • 1,347
  • 16
  • 27
  • 1
    As the wikipedia says in the referenced article, it is important to note that: "the vertices are assumed to be numbered in order of their occurrence along the polygon's perimeter, and the vertex ( xn, yn ) is assumed to be the same as ( x0, y0 )" – Fgblanch Aug 22 '13 at 11:07
  • 1
    Adjusted to close automatically the polygon if (x0, y0) != (xn, yn) – Myobis Jun 16 '15 at 13:34
  • 1
    I think in the first line `var first = t[0]` it's meant to be `var first = pts[0]` –  Jun 18 '15 at 05:11
  • Thanks for spotting this @JoeRocc – Myobis Jun 18 '15 at 11:06
  • This method shows a different result from Google Maps (bounds.getCenter) and MySQL (Centroid function)... – Jessé Catrinck Feb 20 '16 at 14:42
  • @JesséCatrinck Because bounds is the rectangle of the polygon boundaries. Bounds is like min max of x and y of all polygon points. And center the (max-min) / 2. – Domske Sep 04 '18 at 17:01
  • Do we really need first point == last point? In my case it works without this. Because `i` and `j` rotates through all points. – Domske Sep 04 '18 at 17:17
  • 2
    before `pts.push(first)` it is advisable to clone array `pts = [...pts]` – ZoomAll Jun 28 '21 at 17:06
13

The accepted answer has an issue which becomes prominent as the polygon's area becomes smaller. It would not be visible in most cases, but can result in some weird results at very small dimensions. Here's an update to that solution to account for this issue.

function get_polygon_centroid(pts) {
   var first = pts[0], last = pts[pts.length-1];
   if (first.x != last.x || first.y != last.y) pts.push(first);
   var twicearea=0,
   x=0, y=0,
   nPts = pts.length,
   p1, p2, f;
   for ( var i=0, j=nPts-1 ; i<nPts ; j=i++ ) {
      p1 = pts[i]; p2 = pts[j];
      f = (p1.y - first.y) * (p2.x - first.x) - (p2.y - first.y) * (p1.x - first.x);
      twicearea += f;
      x += (p1.x + p2.x - 2 * first.x) * f;
      y += (p1.y + p2.y - 2 * first.y) * f;
   }
   f = twicearea * 3;
   return { x:x/f + first.x, y:y/f + first.y };
}

Here's a case of a centroid ending up outside of a small polygon for anyone curious as to what I'm talking about:

var points = [
    {x:78.0001462, y: 40.0008827},
    {x:78.0000228, y: 40.0008940},
    {x:78.0000242, y: 40.0009264},
    {x:78.0001462, y: 40.0008827},
];
// original get_polygon_centroid(points)
// results in { x: 77.99957948181007, y: 40.00065236005001 }
console.log(get_polygon_centroid(points))
// result is { x: 78.0000644, y: 40.000901033333335 }
pragmar
  • 1,004
  • 9
  • 13
  • 3
    +1 for handling an actual problem with the basic method, surprisingly not mentioned on Wikipedia and other places I checked. However, there's a mistake in the solution: The lines starting with `x += (p1.y + p2.y ...` and `y += (p1.x + p2.x ...` should of course be `x += (p1.x + p2.x ...` and `y += (p1.y + p2.y ...`. – Jonas Jan 09 '18 at 11:23
  • Yikes! Thanks for catching that, Jonas. Fixed. – pragmar Jan 15 '18 at 22:51
  • What causes that issue? Rounding errors? – Sphinxxx Oct 25 '21 at 19:06
  • python version: https://gist.github.com/marcbln/4fc3d5179d06adb5a82551db43d80b07 – qwertz Apr 13 '22 at 18:31
5

If you want the label to be positioned within the concave irregular polygon, then the above answers do not work very well. The centroid of a concave polygon could be very likely outside the polygon. enter image description here

There's this Polylabel library that addresses the problem very well. This blog article gives the details of the algorithm. If you want to use the library in browser and don't want to package it yourself, you can download the library from this page.

Code example to use the library:

function get_polygon_centroid(points) {
    var pts = points.map(p => [p.x, p.y]);  // convert {x:x, y:y} into [x, y]
    var centroid = polylabel([pts]);
    return {x:centroid[0], y:centroid[1]};
}
var my_points = [{x:3,y:1},{x:5,y:8},{x:2,y:9}];
console.log('centroid', get_polygon_centroid(my_points));
Henry Luo
  • 354
  • 4
  • 10
4

This is fairly simple to do. The centroid of a finite set of k points x1, x2, ... xk is described by the formula

(x1 + x2 + ... + xk) / k

That means we can just add all the points up and then divide by the number of points, like this:

function getPolygonCentroid(points){ 
  var centroid = {x: 0, y: 0};
  for(var i = 0; i < points.length; i++) {
     var point = points[i];
     centroid.x += point.x;
     centroid.y += point.y;
  }
  centroid.x /= points.length;
  centroid.y /= points.length;
  return centroid;
} 
csuwldcat
  • 8,021
  • 2
  • 37
  • 32
Peter Olson
  • 139,199
  • 49
  • 202
  • 242
  • 13
    -1: Centroid of a polygon, given in the same wiki page, is different from the centroid of the points that form it. Your definition of the centroid of a number of points is correct. But the centroid of any area is defined as the location of its center of mass, which will in general differ from the centroid of the vertices. – Abhranil Das Mar 31 '12 at 22:07
  • I tried to correct the missing "i" in one of the "points" references, but the stupid editor required that I change at least 6 characters, so I changed the method name in the example to be camelcased (that's more common in JS anyway) - hopefully that's ok Peter Olson? (I didn't know how else to make the change so the example worked without throwing an error due to the misspelling) – csuwldcat Oct 18 '13 at 16:09
2

If you are not casual about the definition of 'centroid', this is the formula for the centroid of a polygon. As you can see, it's sufficiently more complicated than the centroid of a set of points. If you can do with the centroid of the points, that's fine, but if you want the centroid of the polygon, you'd have to implement this formula, which btw is not very difficult. Please remember that in the general case of an irregular polygon, which is your case, these two centroids will be different (otherwise this formula wouldn't exist).

Abhranil Das
  • 5,702
  • 6
  • 35
  • 42
  • 1
    The centroid you give is still the centroid of a set of points; the set of points is just infinite... :) – ellisbben Apr 01 '12 at 21:43
  • 1
    Sure, I just didn't want to elaborate on it too much. In the other case we consider the points to be mass points of equal mass, and here we consider the mass to be distributed uniformly within the polygon. – Abhranil Das Apr 02 '12 at 04:56
0

Building on Myobis' and pragmar's answers, it's not necessary to alter the array by duplicating the first element. In fact, in their implementation the first iteration of the for loop doesn't do anything because pts[i] and pts[j] are identical.

Here is the same algorithm with some optimizations (originally ported from Finding the centroid of a polygon?):

function get_polygon_centroid(points) {
    //Correction for very small polygons:
    const x0 = points[0].x , y0 = points[0].y;

    let x = 0, y = 0, twiceArea = 0;

    let prev = points[points.length - 1];
    for (const next of points)
    {
        const x1 = prev.x - x0, y1 = prev.y - y0,
              x2 = next.x - x0, y2 = next.y - y0,
              a  = x1 * y2 - x2 * y1;

        twiceArea += a;
        x += (x1 + x2) * a;
        y += (y1 + y2) * a;

        prev = next;
    }

    const factor = 3 * twiceArea;  // 6 * twiceArea/2
    x /= factor;
    y /= factor;

    return { x: x + x0, y: y + y0 };
}

const points = [
    { x: 78.0001462, y: 40.0008827 },
    { x: 78.0000228, y: 40.0008940 },
    { x: 78.0000242, y: 40.0009264 },
];
console.log(get_polygon_centroid(points));
Sphinxxx
  • 12,484
  • 4
  • 54
  • 84