Overlaying a bubble chart onto a Google map

Others may hate, but I’m a big fan of using bubbles to display data. When implemented correctly (i.e. scaled in terms of area instead of diameter), bubbles can be an aesthetically appealing and concise way to represent the value of data points in an inherently visual format. Bubbles are even more useful when they include interactivity, with events like mouseover and zoom allowing users to drill down and compare similar-sized bubbles more easily than they can in static graphics. So, when I was recently working on a class project on autism diagnoses in New York City, I decided to use bubbles to represent the percentage of students with individualized education plans at all 1250 or so K-8 New York City schools.

Almost by default, I turned to Google Maps JavaScript API V3, mainly because I’m quasi-familiar with its basic functions and event handlers (as I point out later in  this post, I didn’t realize that a nifty new service called CartoDB would have automated most of what I was trying to do, albeit without nearly the level of customization). Nonetheless, based on a tutorial from Karl Agius, as well as some infoWindow help from my data viz professor, Susan McGregor, I created the following interactive bubble map of NYC schools based upon the number of special needs, or IEP, students at each school. The larger the bubble, the greater percentage of special needs students a school has. Click here to see the map full-screen, or here to download a .zip of my source files for your own customization purposes.

Each bubble on this map represents one of New York City’s approximately 1,250 K-8 public schools, including charters. The larger the bubble, the higher the percentage of students with individualized education plans (IEP). Click on a bubble to find out more about the school, or click anywhere within a district boundary to see an overall average IEP rate for the district. Zoom and pan to see other parts of the city.

You’ll notice the opacity for the bubbles is set to 40 percent. This allows us to get a quick visual of the locations with the highest density of special needs students, given that those areas on the map will naturally be darker because they have multiple semi-opaque layers that overlap. Setting a low opacity also prevents overlapping bubbles from covering up one another. You’ll also notice that the map includes polygons for each school district, which you can click on to get an average IEP rate for the entire district. I decided against setting gradated fill colors for the school district shapes so as to avoid implying causation, as well as to lessen the visual clutter.*

Preparing the data

To create the map, I first had to download the underlying data from the New York City Department of Education database as a .csv, then import it into Excel to clean it up and leave only the relevant information. Although the dataset I had only included street addresses split into multiple columns, I was able to use the concatenate function in Excel to merge the street, city, state and zip columns to get a full street address. From there, I used my favorite batch geocoding service to convert the addresses into geographic coordinates that the Google Maps API can read. Check out my resulting .csv file here for an example. Then I imported the .csv into a Google Spreadsheet, and pasted the resulting spreadsheet’s URL into dataSourceURL field in the JavaScript of my main index.html file. Here’s how that looked in my code:

var dataSourceUrl = "https://docs.google.com/spreadsheet/ccc?key=0Au4D8Alccn4xdHNJdXJmeTdYcEtpRXE1QXRucWtEN3c";
var onlyInfoWindow;

google.load("visualization", "1");
google.load("maps", "3", {other_params:"sensor=false"});

google.setOnLoadCallback(getData);

Drawing the bubbles

Using the ‘Circle’ marker style from the Google Maps JavaScript V3 API, I set each row in the spreadsheet to draw a bubble whose size corresponded to its value. It works by calling up the ‘size’ column in my spreadsheet, which contains the IEP data I want to display, and then drawing an overlay of bubbles based upon those values. Credit again to Karl Agius for helping me figure out this part of the script.

// This first array is where you can change the size and styling of the bubbles.
function Bubble(data, options) {
    this.data = data;
    if (options) 
        this.options = options 
    else 
        this.options = {
            radiusForPercentage:1000,
            text:{
                visible:false,
                minimumZoom:13,
                maximumZoom:15
            },
            bubble:{
                fill:{color:"#709ED9", opacity:0.6},
                stroke:{color:"#709ED9", weight:1, opacity:0.3}
            }
        };
    this.totalSize = this.getTotalSize(this.data);
}

Bubble.prototype = new google.maps.OverlayView;

Bubble.prototype.onAdd = function() {
    for (var row = 0; row < this.data.getNumberOfRows(); row++) 
        this.drawBubble(this.data, this.options, row);
}

Bubble.prototype.draw = function() {
    if (this.options.text.visible)
        for (var row = 0; row < this.data.getNumberOfRows(); row++) 
            this.drawText(this.data, this.options, row);
}

Bubble.prototype.getTotalSize = function(data) {
    var totalSize = 0;
    
    for (var row = 0; row < data.getNumberOfRows(); row++)
        totalSize += data.getValue(row, 1);
                
    return totalSize;
}

Bubble.prototype.drawBubble = function(data, options, row) {
    
    var sizeOfLocation = data.getValue(row, 1);
    var percentageOfTotal = (sizeOfLocation / this.totalSize) * 100;
            
    var radiusForLocation = options.radiusForPercentage * percentageOfTotal;
            
    var marker = new google.maps.Circle({
        center: new google.maps.LatLng(data.getValue(row, 2), data.getValue(row, 3)),
        fillColor:options.bubble.fill.color,
        fillOpacity:options.bubble.fill.opacity,
        strokeColour:options.bubble.stroke.color,
        strokeWeight:options.bubble.stroke.weight,
        strokeOpacity:options.bubble.stroke.opacity,
        radius:radiusForLocation
 });
    // This last command actually places the Circle markers we just made on the map.
    marker.setMap(this.getMap());

You can change the default size of the bubbles with the radiusForPercentage value, and set the colors using the fill and stroke fields. You can also set a minimum and maximum zoom so that users can only see the part of the map you want them to see.

Adding interactivity

Next, to make the bubbles clickable, I added an on-click event listener that set each bubble marker to fetch the corresponding data from my Google spreadsheet and add it to an infoWindow. Here's what that looked like:

var contentString = '
School Name' + data.getValue(row, 0) + 'Percent IEP students:'+sizeOfLocation+'Percent free-lunch:' + data.getValue(row, 4) + '
'; var infowindow = new google.maps.InfoWindow({ content: contentString, position: new google.maps.LatLng(data.getValue(row, 2), data.getValue(row, 3)) }); google.maps.event.addListener(marker, 'click', function() { if(onlyInfoWindow != null){ onlyInfoWindow.close(); } infowindow.open(this.getMap()); onlyInfoWindow = infowindow; });

As you can see, I referenced each value in my spreadsheet by adding in its row number. Later, I went back in and added another row of data that displayed free-lunch statistics as well, which is why you see a reference to row 4.

Overlaying the school district polygons

To place the school polygons onto the map, I added a new overlay that pointed to a Google Fusions table containing the KML geographic boundaries. This is what that looked like:

var schoolDistricts = new google.maps.LatLng(41.850033, -87.6500523);

var layer = new google.maps.FusionTablesLayer({
  query: {
    select: 'geometry',
    from: '3621394'
  },
  styles: [{
  polygonOptions: {
    fillColor: "#FAFBFF",
    fillOpacity: 0
    }
  }
  ]}
  );

layer.setMap(map);
  }

The 'from' field should correspond to your Fusions Table's six-digit ID, and the 'select' field should reference the column in your table that contains the KML geographic data.

A simpler new tool to do roughly the same thing

Ultimately, however, it may have been easier to use the WYSIWYG editor from CartoDB, a premium mapping service with a front-end interface similar to Google Fusions Tables except that it allows you to draw bubbles without having to dig into the API. See what happened when I uploaded my spreadsheet to CartoDB and set similar style attributes:

Which map is better? Personally I think the hand-coded Google Maps API version is better in terms of aesthetics and usability. But was it worth the extra effort? You tell me.

*Jennifer LaFleur, ProPublica's director of computer-assisted reporting, later pointed out to me that I could easily add a gradated fill for each of the school district polygons based upon poverty levels, provided that I pick the right color-scheme to complement the blue bubbles. I guess I was just a bit leery from a design standpoint about what one color on top of another would look like.

12 Comments Overlaying a bubble chart onto a Google map

    1. Carl V. Lewis

      Good question, Aref. What I believe you’d need to do to accomplish this is to add a new overlay to the map by creating another instance of the same JavaScript function. If you add the data points for the new layer into a new column in the same Google spreadsheet, you can then call up that column by changing this bit of script here:
      var sizeOfLocation = data.getValue(row, 1);
      to this:
      var sizeOfLocation = data.getValue(row, 2);.
      That way, the new overlay would draw the values from the second column, and you could style those bubbles however you like.

      Reply
  1. Jeff Franzen

    Excellent work and post. This simple task of placing bubble data on a map has been the holy grail to me for years. It is incredibly cumbersome with API’s and Java Script.

    I am really surprised Google doesn’t make it possible to simply (maybe they do?) put data in a spreadsheet and link to zip codes?

    I am trying Cartodb but it is still somewhat greek to me when I have two simple data points in columns (Zip Code and Amount) and several rows of data.

    Any suggestions? Google Fusion?

    Reply
    1. Carl V. Lewis

      Hi Jeff, super-belated response here, as I’ve only recently had time to get back into blogging (had a super-intense work schedule in 2013). Anyhow, CartoDB now supports this feature automagically within their CMS but without on:mouseover capabilities out-of-the-box. But, my grad school colleague Michael Keller created an awesome GitHub repo a while back with templates for CartoDB bubble chart maps including mouseover tooltips if you want to grab that (https://github.com/mhkeller).

      Reply
  2. David Kurnik

    Apologies for the necro post wanted to compliment your work and ask a question. This is great stuff.

    If I read this right the bubbles varied in radius based on the count of students. Given the area of a circle is =pi()r^2 a linear progression in radius scales the visual effect of the bubble logarithmically. Was this the intent, or did I miss something?

    Thanks,

    –David

    Reply
    1. Carl V. Lewis

      David–

      Terribly late reply here, but you raise an excellent point that I now preach vigorously. It is the area that should depict the value sizes, not the radius, so the visual effect is slightly off (unfortunately, Google’s API didn’t have an easy way to calculate area). CartoDB’s API previously had the same problem (plotting values by radius is a much more straightforward way to render the shapes than area), but it has now fixed in its API, and it’s something I’d been bothered by but forgot to mention in the post.

      Best,
      CVL

      Reply
  3. Pedro

    Carl, you have no idea of how helpful you were. I’m currently trying to map the incidence of several diseases over the world, and this is just the right tool. Thank you so much! Good luck!

    Reply

Leave a Reply