Dynamically Labeled Markers on a Google Map

A project came up recently that involved building a Google Map with a set of markers on it in an Angular single page app. All of the markers needed to be labeled with their rank number. Markers could be red or blue and “inactive” markers would have a reduced opacity (depending on the data). A given map could have as many as 400 markers on it at a time.

The default markers for Google Maps only allow a one character label. This wouldn’t work for this project since we needed up to 3 characters (to show the numbers 1-400).

The common way to make labeled map markers is to create a set of images (either individual or collected on a sprite plate) with the number written on each marker image. If the images are in separate files, you must name them carefully so that the file name can be generated programmatically. However, loading a large number of individual files would be terrible for network performance. Placing all of the images on a sprite plate is likely to be more performant but may result in a very large file and you need to determine how to calculate the position of each image.

Due to the number of markers and that fact that they could be different colors or opacities (1600 combinations!), I decided to investigate other approaches.

After some research, I found that Google Maps will accept an SVG image as an icon. If you’re not familiar, Scalable Vector Graphics (SVG) is an XML specification for describing images. Typically, you don’t write SVG by hand, you draw the image in a graphics program and export it as an SVG file. The resulting file is human readable and looks a lot like HTML.

Since an SVG file is simply a text file, I was able to read in the file and build my set of markers using standard JavaScript string replacement functions.

Below is an example of an SVG marker that I created in Inkscape. Inkscape is a free/open source SVG drawing tool.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg3336"
   width="20" height="29" viewBox="0 0 20.5 29.6">
  <metadata id="metadata3345">
    <rdf:RDF>
      <cc:Work rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title></dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <defs id="defs3343" />
  <path
     style="fill:#0000ff;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
     d="m 3.36225,0.34301 14,0 c 1.662,0 3,1.338 3,3 l 0,14 c 0,1.662 -1.338,3 -3,3 l -3.5,0 c -2.990515,11.902679 -3.666916,11.864088 -7,0 l -3.5,0 c -1.662,0 -3,-1.338 -3,-3 l 0,-14 c 0,-1.662 1.338,-3 3,-3 z"
     id="map-pin" />
  <text id="text3339" text-anchor="middle" font-size="12"
     style="font-family: Arial, sans-serif;font-weight:bold;text-align:center;"
     fill="#fff" transform="translate(10 14.5)">AAA</text>
</svg>

Most of the top of the file is headers and meta data. The only important piece in the top half of the file is the viewBox attribute of the svg tag. This will be used later when scaling and anchoring your marker for Google Maps.

The path and text tags are where all the magic happens. path gives directions for drawing (in the d attribute) and styling (in the style attribute) your marker. text gives positioning and styling information for the label on the marker. I put in some placeholder values in both tags so that I can find and replace them later in my code.

I built a sample Angular project that shows how the SVG marker may be used. It uses Angular UI’s Google Map directives to build the map and markers. The code may be found in this repo.

The HTML to create the map and markers look like this:

<ui-gmap-google-map center='vm.map.center' zoom='vm.map.zoom'>
    <ui-gmap-markers
            models='vm.markers'
            idKey="'id'"
            type="'spider'"
            fit="true"
            coords="'coords'"
            options="'mapOptions'"
            >
    </ui-gmap-markers>
</ui-gmap-google-map>

ui-gmap-google-map creates the map. It is provided with a map center and the level of zoom.

ui-gmap-markers creates all the map markers. It is provided with an array of marker objects using the models attribute. The rest of the attributes on the tag are either values for the directive (ie, type and fit) or they are the names of properties that can be found in the marker objects. idKey is the name of the property on the marker object that holds a unique id in each marker object. coords is the name of the property on the marker object that holds an object containing the coordinates of the marker. options is the name of the property on the marker object that holds an object containing the icon and other options for the marker. type tells the directive what to do if your map markers are close together and overlap. fit is a boolean that tells the map if it should zoom to fit all of the markers on the screen or not.

Here is an example of a markers array:

var markers = [{
    id: 1,
    coords: {
        latitude: 45,
        longitude: -73
    },
    mapOptions: {
        icon: {
            url: 'data:image/svg+xml,' + tempString.split('AAA').join(1),
            size: new google.maps.Size(21, 30),
            scaledSize: new google.maps.Size(21, 30),
            anchor: new google.maps.Point(10.5, 30)
        },
        zIndex: 1000
    }
}];

You can see in this example how the 'AAA' placeholder is replaced with a 1 in the SVG image (held in tempString variable). You can also see how the values from the viewBox attribute of the SVG image were rounded up (the Google functions expect integers but the viewBox had floating numbers) and used for the size, scaledSize, and anchor properties.

See the repo for full code showing 400 randomly positioned and colored markers with dynamically generated labels.

6 Comments

  1. Matt on September 29, 2015 at 8:44 am

    Nice solution! This looks like a really good use case for SVG. One change that I would suggest is to use ‘_number-placeholder_’ or something similar rather than ‘AAA’ to make the template intent more explicit.

    • Garrett McCullough on September 29, 2015 at 9:47 am

      That’s a good idea, Matt. I’d originally used AAA when I created the icon in the image editor so that I could visually check the size of the icon and the placement of the text. But since that is accomplished, it would be a good idea to change the placeholder.

  2. Mike on January 19, 2016 at 8:51 am

    Is there an example of this live on the web anywhere?

    • Garrett McCullough on January 19, 2016 at 12:13 pm

      Sorry, I don’t have a live example that I can offer you. The original project where I used this is behind a login wall. The code in the repo is working, so you could clone it to your machine and run it either locally or by uploading it to a server.

      • Matthieu Gourvès on February 23, 2016 at 12:59 am

        Hi Garrett
        I’ve downloaded your code from the repo but I can’t get it work. The file dist/app.js is missing.
        Any idea ?
        Thanks a lot anyway

        • Garrett McCullough on February 23, 2016 at 9:41 am

          Hi Matthieu,

          The code was written with ES6 JavaScript that only the newest browsers support and used Babel to transpile it into the ES5 JavaScript that all browsers support. If you want to learn more about how that works, see this blog post:
          http://mcculloughwebservices.com/2015/12/10/webstorm-babel-6-plugin/

          To make it easier, I updated the code repo with the files in the dist/ folder, so you should be able to re-download it and run it.

          Sorry about the trouble. Let me know if you have any other questions.

Leave a Comment