A few weeks ago, a club I’m a member of was updating its membership information. Since the data was being collected in a Google spread sheet, I thought it would be interesting to create a map visualization to show where the members come from. In this post, we’ll write a map overlay which will generate a display like the one in the following image using data drawn from a Google spread sheet. Basically, we’re going to give a map a nasty rash.
You can also see the map here, or download the files here. Note that the size of each location has been fudged and bears no relation to the values originally collected in the membership survey.
A note on Access Control
Since the data resides in a Google document, the user’s browser would need to be able to access that document for an overlay to be displayed. For the purposes of this document, our dummy data has been made publicly viewable. In your case of course, you may not want to expose your data to the world, so you should just share out the data to the people who need it. They will be able to view the overlay if they’re already signed into a Google account you’ve given access to.
Getting started
For the rest of this post, I’m going to assume you already know how to display and control a map via JavaScript. If not, a quick visit to the documentation for the Map API is in order. Don’t worry, it’s quite concise. We’ll be using version 3 of the API.
We’ll also be using the Visualization API (version 1) to deal with our data source. This is loaded in the same manner as any other API hosted by Google:
1: google.load("visualization", "1");
After which we can ask it to load any spread sheet we need.
1: var query = new google.visualization.Query(dataSourceUrl);
2: query.send(handleQueryResponse);
Although I’m using the spread sheet URL here, there’s no reason to load the whole thing – I’m only doing that here because there isn’t anything else in the spread sheet. The query object itself has a setQuery method which can take expressions as needed.
The query’s send method takes a callback, which is where we tell it to display our map and create the overlay with all the bubbles.
Creating the Overlay
A custom overlay is created by extending the OverlayView class and defining its drawing behaviour. Since we’ll be using map markers, we won’t need to constantly keep them updated when the view is zoomed and panned – the API already handles that. We can just create the markers in the onAdd function, and we’re set:
1: Bubble.prototype.onAdd = function() {
2: for (var row = 0; row < this.data.getNumberOfRows(); row++)
3: this.drawBubble(this.data, this.options, row, this.totalSize);
4: }
The drawBubble function then creates a marker of the appropriate size and sticks it to the map:
1: var marker = new google.maps.Circle({
2: center: new google.maps.LatLng(data.getValue(row, 2), data.getValue(row, 3)),
3: fillColor:options.bubble.fill.color,
4: fillOpacity:options.bubble.fill.opacity,
5: strokeColour:options.bubble.stroke.color,
6: strokeWeight:options.bubble.stroke.weight,
7: strokeOpacity:options.bubble.stroke.opacity,
8: radius:radiusForLocation
9: });
10:
11: marker.setMap(this.getMap());
Since the radius of the marker is expressed in an actual real world size (it’s a variable number of metres multiplied by the percentage size of the locality; so if the variable is 10 and the locality represents 20% of the population, then the bubble will be 200m in radius), it will scale with the map.
The problem with text
Unlike markers, text is not supported directly by the API, which means we’re SOL if we want to add labels quickly. Luckily, we can still add labels as html elements, and pin them on to the map using absolute positioning – the fromLatLngToDivPixel allows us to change a Latitude and Longitude value into a pixel coordinate on our map display, so we can put text up to our hearts’ content:
1: if (!textContainer) {
2: var textItem = document.createElement('span');
3: textItem.className = 'label';
4: textItem.innerHTML = data.getValue(row, 0) + ' (' + data.getValue(row, 1) + ')';
5:
6: textContainer = document.createElement('div');
7: textContainer.id = itemId;
8: textContainer.appendChild(textItem);
9: textContainer.style.cssText = 'position: absolute;';
10:
11: var panes = this.getPanes();
12: panes.overlayLayer.appendChild(textContainer);
13: }
14:
15: var projection = this.getProjection();
16: var position = projection.fromLatLngToDivPixel(new google.maps.LatLng(data.getValue(row, 2), data.getValue(row, 3)));
17:
18: textContainer.style.left = position.x+ 'px';
19: textContainer.style.top = position.y + 'px';
Note the check for null values before creating a new text container. Since the text elements are not fire and forget, we have to look after them ourselves, as they will not scale; since they’re positioned in relation to a pixel coordinate, they’ll also stay put regardless of how much we pan the map. To have the label maintain some kind of meaningful connection to whatever it’s labelling, we need to keep this up to date with the movements of our map – something which we can achieve with the draw function, which is called after onAdd, and any time the map is scaled or moved:
1: Bubble.prototype.draw = function() {
2: if (this.options.text.visible)
3: for (var row = 0; row < this.data.getNumberOfRows(); row++)
4: this.drawText(this.data, this.options, row);
5: }
Making bubbles
To add the bubbles to a map, first get the data source, then create an instance of the overlay and the map. Finally, attach it to the map:
1: var bubbleMap = new Bubble(data);
2: bubbleMap.setMap(map);
Defining the data in your script
If you don’t want to use a Google spread sheet as a data source, and you can always define the data yourself. You can set up the data as follows:
1: var data = new google.visualization.DataTable();
2: data.addColumn('string', 'Location');
3: data.addColumn('number', 'Size');
4: data.addColumn('number', 'Latitude');
5: data.addColumn('number', 'Longitude');
6:
7: data.addRows([
8: ['Mosta', 2, 35.909722, 14.426111],
9: ['Fgura', 4, 35.87208146578945, 14.522767066955566],
10: ['Qrendi', 1, 35.832242, 14.448051],
11: ['Birkirkara', 2, 35.896667, 14.4625],
12: ['Sta. Venera', 1, 35.889722, 14.477778],
13: ['Zabbar', 1, 35.87639350716732, 14.540534019470215],
14: ['Zejtun', 2, 35.855556, 14.533333],
15: ['Gzira', 1, 35.905006994334805, 14.4944429397583],
16: ['Kalkara', 2, 35.889167, 14.529444],
17: ['Bugibba', 1, 35.949963, 14.417225],
18: ['Hamrun', 1, 35.88609474213613, 14.48946475982666]
19: ]);
Once the rows have been added, you can use the data object in the same way as one you would have received in response to query.send.
Options
The constructor for the bubble overlay takes an optional parameter which controls the way the display is built up. The defaults are as follows:
1: {
2: radiusForPercentage:100,
3: text:{
4: visible:true,
5: minimumZoom:12
6: },
7: bubble:{
8: fill:{color:"#FF0000", opacity:0.4},
9: stroke:{color:"#FF0000", weight:1, opacity:0.3}
10: }
11: }
radiusForPercentage defines how large each bubble, and is given in metres. Each 1% of the total population (see above) will increase the bubble radius by this amount.
text.visible determines whether text will be displayed, or not.
text.minimumZoom tells you how close you have to zoom in for text labels to be displayed. This prevents smaller maps (such as the one I’m using in this example) from becoming unreadable clutters of alphabet soup gone wrong.
The bubble parameters should be fairly self explanatory.
As usual, any suggestions would be welcome, though there’s no guarantee I’ll actually do anything about them.
This is very good mate! Thank you for this, I will give it a spin tomorrow during a more socially acceptable hour 🙂
Thanks Stefan. Still looking into a few tweaks for this (specifically, animation and partial loading of data based on the bounds of the viewport), though it may be a while before I have time to touch it again.
do you know of any dataset/datasource which have all coordinates of malta villages borders?I want to seperate villages by using the borders.
thanks
Hi Michael… I’m afraid I’m not aware that such a data set exists, though I haven’t searched for one in depth. You might be able to plot one by getting a map with the borders and use that to calculate the coordinates of the borders.
Hi ,
Do you know why i can’t change the id of map division to other than map , i have tried to change in
var map = new google.maps.Map(document.getElementById(“map123”), mapping); and also changed the name of div to same but its not working , any idea?
Hi Jijo, thanks for your message! Just tried this; you also need to change the css on line 4 to
#map123 { width:50%; height:50%; }
That sets the size of the map div, otherwise it defaults to width 100%, height 0. Hope that helps!