In this codelab, you’ll create a visualization of thousands of data points over an interactive Google Map, taking advantage of Polymer and some Google Web Components to easily cobble together a pipeline to load data, pass it into WebGL, and then manipulate it in real time. All WebGL code is already included and working, leaving you free to tinker with it as you see fit.

What you’ll learn

What you’ll need

How would rate your experience with Polymer?

NoviceIntermediateAdvanced

Create a new project

The first time you run Chrome Dev Editor it will ask you to setup your workspace environment.

Fire up Chrome Dev Editor and start a new project:

  1. Click to start a new project.
  2. Enter "PolymerVizCodelab" as the Project name.
  3. In the Project type dropdown, select "JavaScript web app (using Polymer paper elements)".
  4. Click the Create button.

Chrome Dev Editor creates a basic scaffold for your Polymer app. In the background, it also uses Bower to download and install a list of dependencies (including the Polymer core library) into the bower_components/ folder. Fetching the components may take some time if your internet connection is slow. You'll learn more about using Bower in the next step.

Your folder structure should look like this:

PolymerVizCodelab/
  bower_components/ <!-- installed dependencies from Bower -->
  bower.json  <!-- Bower metadata files used for managing deps -->
  index.html  <!-- your app -->
  main.js
  styles.css

Preview the app

At any point, select the index.html file and hit the  button in the top toolbar to run the app. Chrome Dev Editor fires up a web server and navigates to the index.html page. This is great way to preview changes as you make them.

Next up

At this point the app doesn't do much. Let's add a map and draw something on it.

The Google Web Components provide the <google-map> element for declaratively rendering a Google Map. To use it, you first need to install it using Bower.

Install the element

Normally, you'd run bower install GoogleWebComponents/google-map --save on the command line to install <google-map> and save it as a dependency. However, Chrome Dev Editor does not have a command line for running Bower commands. Instead, you need to manually edit bower.json to include google-map, then run Chrome Dev Editor's Bower Update feature. Bower Update checks the dependencies in bower.json and installs any missing ones.

  1. Edit bower.json and add google-map to the dependencies object:
"dependencies": {
  "iron-elements": "PolymerElements/iron-elements#^1.0.0",
  "paper-elements": "PolymerElements/paper-elements#^1.0.1",
  "google-map": "GoogleWebComponents/google-map#^1.1.1"
}
  1. Right-click the bower.json filename in the editor.
  2. Select Bower Update from the context menu.

The download may take few seconds. You can verify that <google-map> (and any dependencies) were installed by checking that bower_components/google-map/ were created and populated.

Use the element

To employ <google-map>, you need to:

  1. Use an HTML Import to load it in index.html.
  2. Declare an instance of the element on the page.

In index.html, remove all other HTML imports in the <head> and replace them with a single import that loads google-map.html:

index.html

<head>
  ....
  <script src="bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
  <link rel="import" href="bower_components/google-map/google-map.html">

  <link rel="stylesheet" href="styles.css">
</head>

Next, replace the contents of <body> with an instance of <google-map>:

index.html

<body unresolved>
  <google-map latitude="37.779" longitude="-122.3892" zoom="13"></google-map>
</body>

As you can see, using <google-map> like this is completely declarative. The map is centered using the latitude and longitude attributes and its zoom level is set by the zoom attribute.

Styling the map

If you run the app right now, nothing will display. In order for the map to properly display itself, you need to give its container (in this case, <body>) a height.

Open styles.css and replace its entire contents with default styling:

styles.css

body, html {
  font-family: 'Roboto', Arial, sans-serif;
  height: 100%;
  margin: 0;
}

If you run the app now, you’ll see a map centered on San Francisco, but the default colors of Google Maps are also a little too bright and attention grabbing for our purposes, distracting from the data you will draw over the map. The Google Maps API has an extensive styling API, exposed on <google-map> through the styles attribute, but keep things simple for now by just turning down the saturation and darkening the water.

Replace the simpler <google-map> declaration with:

index.html

<body unresolved>
  <google-map latitude="37.779" longitude="-122.3892" zoom="13"
    styles='[{"stylers":[{"saturation":-85}]},{"featureType":"water","stylers":[{"lightness":-20}]}]'>
  </google-map>
</body>

Run the app

If you haven't already done so, hit the  button. At this point, you should see a map that takes up the entire viewport and gives a desaturated view of San Francisco:

Next up

Add a data overlay to the map and finally visualize something!

We'll be using the <point-overlay> element, which uses the CanvasLayer library to draw data over the map. While the Maps API itself has a number of options for data visualization built into it, CanvasLayer enables WebGL content to be drawn over the map, allowing millions of items to be drawn on the screen in real time.

Install the element

Once again, the Bower dance is needed to fetch the code. Edit bower.json to load <point-overlay>:

bower.json

"dependencies": {
  "iron-elements": "PolymerElements/iron-elements#^1.0.0",
  "paper-elements": "PolymerElements/paper-elements#^1.0.1",
  "google-map": "GoogleWebComponents/google-map#^1.1.1",
  "point-overlay": "googlecodelabs/polymer-webgl-visualization"
}

Then once again right click bower.json on the left-hand side and select “Bower Update”.

To employ <point-overlay>:

  1. Use an HTML Import to load it in index.html.
  2. Declare an instance of the element on the page.
  3. "Connect" it to the map so it knows where to draw.

In index.html, add an HTML Import for point-overlay.html:

index.html

<head>
  ....
  <script src="bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
  <link rel="import" href="bower_components/google-map/google-map.html">
  <link rel="import" href="bower_components/point-overlay/point-overlay.html">

  <link rel="stylesheet" href="styles.css">
</head>

Declare <point-overlay> as a sibling of <google-map>. Set its data attribute to '[{"lat": 37.779, "lng": -122.3892}]'. This will the first data point drawn on the map.

index.html

<body unresolved>
  <google-map latitude="37.779" longitude="-122.3892" zoom="13"
    styles='[{"stylers":[{"saturation":-85}]},{"featureType":"water","stylers":[{"lightness":-20}]}]'>
  </google-map>
  <point-overlay data='[{"lat": 37.779, "lng": -122.3892}]'>
  </point-overlay>
</body>

Binding the overlay to the map

<point-overlay> can do very little without a map to draw on. <google-map> elements have a map property that refers to the Google Map instance that they create. <point-overlay> also has a map property, but it doesn’t populate it, it waits until a map object is provided to it there, then uses it for drawing. You can use data binding through Polymer to get these two talking.

Polymer's data-binding features are usually only available when inside a <dom-module>. However, Polymer provides a type-extension version of the <template> element named "dom-bind". Essentially, it allows you to use Polymer sugaring features outside of Polymer. For example, it allows you to use {{}} bindings with elements in your main page.

First, wrap the entire contents of <body> with a <template is="dom-bind">. This will provide a context in which a central map binding property can live. Then bind each element’s map property to that binding property:

index.html

<body unresolved>
  <template is="dom-bind">
    <google-map map="{{map}}" latitude="37.779" longitude="-122.3892" zoom="13"
      styles='[{"stylers":[{"saturation":-85}]},{"featureType":"water","stylers":[{"lightness":-20}]}]'>
    </google-map>
    <point-overlay map="[[map]]" data='[{"lat": 37.779, "lng": -122.3892}]'>
    </point-overlay>
  </template>
</body>

Now, when the <google-map> initializes and its map property changes from null to a map object, <point-overlay>’s map property will also change too. We've used {{map}} as the binding property name, but you can use whatever name you like (for example, map="{{foo}}").

Run the app

Hit the  button! At this point, you should see the map and a first data point, drawn over the map.

Next up

Use Polymer’s data-binding features to control WebGL drawing code from HTML UI.

Let's start by adding a simple slider to start interacting with the WebGL code drawing your (so far) single point. Polymer provides several elements for working with input; we’ll start with <paper-card> and <paper-slider>.

Adding a

The new elements have already been imported by the paper-elements entry in bower.json, so in index.html, add new HTML Imports for these components to the <head>:

index.html

<head>
  ....
  <link rel="import" href="bower_components/point-overlay/point-overlay.html">
  <link rel="import" href="bower_components/paper-card/paper-card.html">
  <link rel="import" href="bower_components/paper-slider/paper-slider.html">

  <link rel="stylesheet" href="styles.css">
</head>

At the bottom of the body, inside the template, add a <paper-slider> inside a material design <paper-card>:

index.html

<template is="dom-bind">
  ...
  <point-overlay map="[[map]]" data='[{"lat": 37.779, "lng": -122.3892}]'>
  </point-overlay>
    
  <paper-card elevation="2">
    <paper-slider min="5" max="100" value="30">
    </paper-slider>
  </paper-card>
</template>

In styles.css, add some simple styling to the card so it displays in the upper right corner, above the map:

styles.css

body, html {
  ...
}

paper-card {
  position: absolute;
  top: 25px;
  right: 25px;
}

Binding the slider to a WebGL uniform

In order to get the slider to do anything, we need to bind its value to the data visualization. <point-overlay> uses WebGL to draw over the map, and WebGL uses shaders to do that drawing.

Shaders have a set of parameters called uniforms that can be set by code running on the CPU to affect the shader running on the GPU. <point-overlay> automatically takes any uniforms declared in the shader code and exposes them on its uniforms property, which makes them available for binding through Polymer. The default <point-overlay> shaders have a uniform called pointSize, which is what you’ll bind to.

Since pointSize is a property on uniforms, you’ll have to use path binding to bind to it:

  1. Bind the <point-overlay>’s uniforms property to a variable (like, say, “uniforms”) on the dom-bound template.
  2. Bind the <paper-slider>’s immediateValue property to the pointSize property on uniforms.

index.html

<template is="dom-bind">
  ...
  <point-overlay map="[[map]]" uniforms="{{uniforms}}" data='[{"lat": 37.779, "lng": -122.3892}]'>
  </point-overlay>

  <paper-card elevation="2">
    <paper-slider min="5" max="100" value="30" immediate-value="{{uniforms.pointSize}}">
    </paper-slider>
  </paper-card>
</template>

Run the app

Hit the  button. At this point, you should be able to control the size of the point with the slider!

Next up

Drawing lots and lots of data.

Included with <point-overlay> is a sample dataset from NOAA’s Storm Prediction Center Severe Weather Database with coordinates of all recorded tornadoes in the (contiguous) United States from 1950 through 2014. <point-overlay>’s data property will happily take any JSON array of objects that have a lat and lng property to know where to draw them, like so:

example_data.json

[
  {
    "lat": 38.77,
    "lng": -90.22,
  },
  {
    "lat": 31.63,
    "lng": -93.65,
  },
  {
    "lat": 26.35,
    "lng": -80.08,
  },
  ...
]

and the sample data in tornadoes-1950-2014.json is in just this format.

Loading the data

To load the data, you can use Polymer’s handy <iron-ajax> element. As before, you’ll need to import it in the head of the page:

index.html

<head>
  ...
  <link rel="import" href="bower_components/paper-slider/paper-slider.html">
  <link rel="import" href="bower_components/iron-ajax/iron-ajax.html">

  <link rel="stylesheet" href="styles.css">
</head>

After the <point-overlay>

  1. add an <iron-ajax> element, setting it to load the JSON file automatically
  2. bind the <point-overlay>’s data attribute to the <iron-ajax> response:

index.html

<template is="dom-bind">
  ...
  <point-overlay map="[[map]]" uniforms="{{uniforms}}" data="{{data}}">
  </point-overlay>
  <iron-ajax auto handle-as="json"
    url="bower_components/point-overlay/tornadoes-1950-2014.json"
    last-response="{{data}}">
  </iron-ajax>
  ...
</template>

Now the <point-overlay>’s data property is bound to the same variable as the <iron-ajax>’s lastResponse property. When the JSON file is loaded, the <point-overlay> will be populated with that data and start drawing.

Finally, you might want to pick a better starting viewpoint for the map so you can see more of the data when the page loads. Here’s one possibility:

index.html

<google-map map="{{map}}" latitude="38.6" longitude="-95.8" zoom="5"
  styles='[{"stylers":[{"saturation":-85}]},{"featureType":"water","stylers":[{"lightness":-20}]}]'>
</google-map>

Run the app

Hit the  button! You should now see every landing point of every American tornado in the last 64 years. For now, you may need to adjust your pointSize slider to see them all clearly at your favorite zoom level.

Next up

Making the points look good at every zoom level by adding a little JavaScript.

Add a pointSize-changing function

There’s no easy mapping from zoom level to pointSize that can be expressed in a Polymer binding, so we’ll use a regular JavaScript function that adjusts the size based on the zoom. The function to be called needs a place to live, and, like the variables binding element properties on our page, the pointSize-changing function can live on the surrounding dom-bind <template> element.

Add a <script> element to the bottom of the body, and in it grab the <template> and add the function:

index.html

<body unresolved>
  <template is="dom-bind" id="t">
    ...
  </template>
  <script>
    var t = document.querySelector('#t');
    t.setPointSize = function(e) {
      this.uniforms.pointSize = Math.exp(0.3 * this.map.getZoom());
      this.notifyPath('uniforms.pointSize', this.uniforms.pointSize);
    };
  </script>
</body>

Note a few things about the change:

Triggering with events

Now setPointSize() just needs to be called at the right times. A computed binding wouldn’t have worked, because pointSize isn’t a property directly on the <point-overlay>. Instead, we’ll rely on events from the <google-map> to drive the pointSize changes.

Since we want this to be run on every change of zoom level, we can use the zoom-changed event on the <google-map> element to trigger the function. Polymer provides a convenient shorthand to set event listeners. By setting an on-event-name attribute (in this case, on-zoom-changed) on <google-map>, Polymer will register our function as a listener for that event automatically:

index.html

<google-map
  ...
  on-zoom-changed="setPointSize">
</google-map>

However, the zoom-changed event only fires when the zoom changes; this still leaves the initial value of uniforms.pointSize unset or at a default value. Like the setting of many initial values on the web, timing presents a problem: we don’t want setPointSize() called too early—Polymer won’t have initialized yet and all our variables will still be undefined and cause an error—and we don’t want it called too late—default-sized points will have already been drawn to screen and users will see a Flash Of Unstyled pointSize (FOUpS).

<google-map> provides another event that solves this problem: google-map-ready, fired when the Maps API has fully loaded and is ready. This is perfect because it means this.map.getZoom() will correctly resolve, but we’re still in the same event-loop turn as the map’s initialization, so nothing has been drawn over the map yet.

Set on-google-map-ready to setPointSize as well:

index.html

<google-map
  ...
  on-google-map-ready="setPointSize"
  on-zoom-changed="setPointSize">
</google-map>

To finish off this step, comment out the <paper-card> and <paper-slider>, since the binding to pointSize no longer makes sense, but you’ll need the slider again in an upcoming step.

All Together Now

All this has required some involved changes, so here’s index.html as a whole to give you an idea what it should look like (give or take any additional changes you’ve made along the way):

index.html

<!doctype html>

<html>
<head>
  <title>PolymerVizCodelab</title>

  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-capable" content="yes">

  <script src="bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>

  <link rel="import" href="bower_components/google-map/google-map.html">
  <link rel="import" href="bower_components/point-overlay/point-overlay.html">
  <link rel="import" href="bower_components/paper-card/paper-card.html">
  <link rel="import" href="bower_components/paper-slider/paper-slider.html">
  <link rel="import" href="bower_components/iron-ajax/iron-ajax.html">

  <link rel="stylesheet" href="styles.css">
</head>

<body unresolved>
  <template is="dom-bind" id="t">
    <google-map map="{{map}}" latitude="38.6" longitude="-95.8" zoom="5"
      styles='[{"stylers":[{"saturation":-85}]},{"featureType":"water","stylers":[{"lightness":-20}]}]'
      on-google-map-ready="setPointSize"
      on-zoom-changed="setPointSize">
    </google-map>
    <point-overlay map="[[map]]" uniforms="{{uniforms}}" data="{{data}}">
    </point-overlay>
    <iron-ajax auto handle-as="json"
      url="bower_components/point-overlay/tornadoes-1950-2014.json"
      last-response="{{data}}">
    </iron-ajax>
    
    <!-- <paper-card elevation="2">
      <paper-slider min="5" max="100" value="30"></paper-slider>
    </paper-card> -->
  </template>
  
  <script>
    var t = document.querySelector('#t');
    t.setPointSize = function(e) {
      this.uniforms.pointSize = Math.exp(0.3 * this.map.getZoom());
      this.notifyPath('uniforms.pointSize', this.uniforms.pointSize);
    };
  </script>
</body>
</html>

Run the app

Hit the  button! With pointSize now managed automatically, the map should now be starting to look good at any zoom level.

Next up

We still have a sea of undifferentiated orange dots. It’s time to style each point based on its unique aspects.

You've built the beginnings of an app to visualize massive amounts of geographic data. <point-overlay> abstracts some of the details away from you, but you can reach into <point-overlay> and get the underlying CanvasLayer object and even access the <canvas> element itself. Everything can be modified from the data you use to the shader code itself.

Now that you have a working start, change anything and everything to see what else you can make.

Learn More

Polymer

Other