Combine and Visualize Multiple Data Sources

Overview

This tutorial shows you how to display data from multiple sources on a Google map. As an example, the choropleth map below uses two different sources to highlight various US states, and display state-specific data.

The map uses data from a GeoJSON file to display polygons that define US state boundaries. It can also present data on the map corresponding to each state, which comes from a simulated query to the US Census API.

Select a category of data from the control dropdown menu to update the polygons on the map. You can also hover over a state polygon to view state-specific information in a data box control on the map.

The sample below shows the entire code you need to create this map.

TypeScript

const mapStyle: google.maps.MapTypeStyle[] = [
{
stylers: [{ visibility: "off" }],
},
{
featureType: "landscape",
elementType: "geometry",
stylers: [{ visibility: "on" }, { color: "#fcfcfc" }],
},
{
featureType: "water",
elementType: "geometry",
stylers: [{ visibility: "on" }, { color: "#bfd4ff" }],
},
];
let map: google.maps.Map;

let censusMin = Number.MAX_VALUE,
censusMax = -Number.MAX_VALUE;

function initMap(): void {
// load the map
map = new google.maps.Map(document.getElementById( "map" ) as HTMLElement, {
center: { lat: 40, lng: -100 },
zoom: 4,
styles: mapStyle,
});

// set up the style rules and events for google.maps.Data
map.data.setStyle(styleFeature);
map.data.addListener( "mouseover", mouseInToRegion);
map.data.addListener( "mouseout", mouseOutOfRegion);

// wire up the button
const selectBox = document.getElementById(
"census-variable"
) as HTMLSelectElement;

google.maps.event.addDomListener(selectBox, "change", () => {
clearCensusData();
loadCensusData(selectBox.options[selectBox.selectedIndex].value);
});

// state polygons only need to be loaded once, do them now
loadMapShapes();
}

/** Loads the state boundary polygons from a GeoJSON source. */
function loadMapShapes() {
// load US state outline polygons from a GeoJson file
map.data.loadGeoJson(
"https://storage.googleapis.com/mapsdevsite/json/states.js",
{ idPropertyName: "STATE" }
);

// wait for the request to complete by listening for the first feature to be
// added
google.maps.event.addListenerOnce(map.data, "addfeature", () => {
google.maps.event.trigger(
document.getElementById( "census-variable" ) as HTMLElement,
"change"
);
});
}

/**
* Loads the census data from a simulated API call to the US Census API.
*
* @param {string} variable
*/
function loadCensusData(variable: string) {
// load the requested variable from the census API (using local copies)
const xhr = new XMLHttpRequest();

xhr.open( "GET", variable + ".json" );

xhr.onload = function () {
const censusData = JSON.parse(xhr.responseText) as any;

censusData.shift(); // the first row contains column names
censusData.forEach((row: string) => {
const censusVariable = parseFloat(row[0]);
const stateId = row[1];

// keep track of min and max values
if (censusVariable < censusMin) {
censusMin = censusVariable;
}

if (censusVariable > censusMax) {
censusMax = censusVariable;
}

const state = map.data.getFeatureById(stateId);

// update the existing row with the new data
if (state) {
state.setProperty( "census_variable", censusVariable);
}
});

// update and display the legend
(document.getElementById( "census-min" ) as HTMLElement).textContent =
censusMin.toLocaleString();
(document.getElementById( "census-max" ) as HTMLElement).textContent =
censusMax.toLocaleString();
};

xhr.send();
}

/** Removes census data from each shape on the map and resets the UI. */
function clearCensusData() {
censusMin = Number.MAX_VALUE;
censusMax = -Number.MAX_VALUE;
map.data.forEach((row) => {
row.setProperty( "census_variable", undefined);
});
(document.getElementById( "data-box" ) as HTMLElement).style.display = "none";
(document.getElementById( "data-caret" ) as HTMLElement).style.display = "none";
}

/**
* Applies a gradient style based on the 'census_variable' column.
* This is the callback passed to data.setStyle() and is called for each row in
* the data set. Check out the docs for Data.StylingFunction.
*
* @param {google.maps.Data.Feature} feature
*/
function styleFeature(feature: google.maps.Data.Feature) {
const low = [5, 69, 54]; // color of smallest datum
const high = [151, 83, 34]; // color of largest datum

let censusVariable = feature.getProperty( "census_variable" ) as number;

// delta represents where the value sits between the min and max
const delta =
(censusVariable - censusMin) /
(censusMax - censusMin);

const color: number[] = [];

for (let i = 0; i < 3; i++) {
// calculate an integer color based on the delta
color.push((high[i] - low[i]) * delta + low[i]);
}

// determine whether to show this shape or not
let showRow = true;

if (
censusVariable == null ||
isNaN(censusVariable)
) {
showRow = false;
}

let outlineWeight = 0.5,
zIndex = 1;

if (feature.getProperty( "state" ) === "hover" ) {
outlineWeight = zIndex = 2;
}

return {
strokeWeight: outlineWeight,
strokeColor: "#fff",
zIndex: zIndex,
fillColor: "hsl(" + color[0] + "," + color[1] + "%," + color[2] + "%)",
fillOpacity: 0.75,
visible: showRow,
};
}

/**
* Responds to the mouse-in event on a map shape (state).
*
* @param {?google.maps.MapMouseEvent} e
*/
function mouseInToRegion(e: any) {
// set the hover state so the setStyle function can change the border
e.feature.setProperty( "state", "hover" );

const percent =
((e.feature.getProperty( "census_variable" ) - censusMin) /
(censusMax - censusMin)) *
100;

// update the label
(document.getElementById( "data-label" ) as HTMLElement).textContent =
e.feature.getProperty( "NAME" );
(document.getElementById( "data-value" ) as HTMLElement).textContent = e.feature
.getProperty( "census_variable" )
.toLocaleString();
(document.getElementById( "data-box" ) as HTMLElement).style.display = "block";
(document.getElementById( "data-caret" ) as HTMLElement).style.display =
"block";
(document.getElementById( "data-caret" ) as HTMLElement).style.paddingLeft =
percent + "%";
}

/**
* Responds to the mouse-out event on a map shape (state).
*
*/
function mouseOutOfRegion(e: any) {
// reset the hover state, returning the border to normal
e.feature.setProperty( "state", "normal" );
}

declare global {
interface Window {
initMap: () => void;
}
}
window.initMap = initMap;

JavaScript

const mapStyle = [
{
stylers: [{ visibility: "off" }],
},
{
featureType: "landscape",
elementType: "geometry",
stylers: [{ visibility: "on" }, { color: "#fcfcfc" }],
},
{
featureType: "water",
elementType: "geometry",
stylers: [{ visibility: "on" }, { color: "#bfd4ff" }],
},
];
let map;
let censusMin = Number.MAX_VALUE,
censusMax = -Number.MAX_VALUE;

function initMap() {
// load the map
map = new google.maps.Map(document.getElementById( "map" ), {
center: { lat: 40, lng: -100 },
zoom: 4,
styles: mapStyle,
});
// set up the style rules and events for google.maps.Data
map.data.setStyle(styleFeature);
map.data.addListener( "mouseover", mouseInToRegion);
map.data.addListener( "mouseout", mouseOutOfRegion);

// wire up the button
const selectBox = document.getElementById( "census-variable" );

google.maps.event.addDomListener(selectBox, "change", () => {
clearCensusData();
loadCensusData(selectBox.options[selectBox.selectedIndex].value);
});
// state polygons only need to be loaded once, do them now
loadMapShapes();
}

/** Loads the state boundary polygons from a GeoJSON source. */
function loadMapShapes() {
// load US state outline polygons from a GeoJson file
map.data.loadGeoJson(
"https://storage.googleapis.com/mapsdevsite/json/states.js",
{ idPropertyName: "STATE" },
);
// wait for the request to complete by listening for the first feature to be
// added
google.maps.event.addListenerOnce(map.data, "addfeature", () => {
google.maps.event.trigger(
document.getElementById( "census-variable" ),
"change",
);
});
}

/**
* Loads the census data from a simulated API call to the US Census API.
*
* @param {string} variable
*/
function loadCensusData(variable) {
// load the requested variable from the census API (using local copies)
const xhr = new XMLHttpRequest();

xhr.open( "GET", variable + ".json" );
xhr.onload = function () {
const censusData = JSON.parse(xhr.responseText);

censusData.shift(); // the first row contains column names
censusData.forEach((row) => {
const censusVariable = parseFloat(row[0]);
const stateId = row[1];

// keep track of min and max values
if (censusVariable < censusMin) {
censusMin = censusVariable;
}

if (censusVariable > censusMax) {
censusMax = censusVariable;
}

const state = map.data.getFeatureById(stateId);

// update the existing row with the new data
if (state) {
state.setProperty( "census_variable", censusVariable);
}
});
// update and display the legend
document.getElementById( "census-min" ).textContent =
censusMin.toLocaleString();
document.getElementById( "census-max" ).textContent =
censusMax.toLocaleString();
};

xhr.send();
}

/** Removes census data from each shape on the map and resets the UI. */
function clearCensusData() {
censusMin = Number.MAX_VALUE;
censusMax = -Number.MAX_VALUE;
map.data.forEach((row) => {
row.setProperty( "census_variable", undefined);
});
document.getElementById( "data-box" ).style.display = "none";
document.getElementById( "data-caret" ).style.display = "none";
}

/**
* Applies a gradient style based on the 'census_variable' column.
* This is the callback passed to data.setStyle() and is called for each row in
* the data set. Check out the docs for Data.StylingFunction.
*
* @param {google.maps.Data.Feature} feature
*/
function styleFeature(feature) {
const low = [5, 69, 54]; // color of smallest datum
const high = [151, 83, 34]; // color of largest datum
let censusVariable = feature.getProperty( "census_variable" );
// delta represents where the value sits between the min and max
const delta = (censusVariable - censusMin) / (censusMax - censusMin);
const color = [];

for (let i = 0; i < 3; i++) {
// calculate an integer color based on the delta
color.push((high[i] - low[i]) * delta + low[i]);
}

// determine whether to show this shape or not
let showRow = true;

if (censusVariable == null || isNaN(censusVariable)) {
showRow = false;
}

let outlineWeight = 0.5,
zIndex = 1;

if (feature.getProperty( "state" ) === "hover" ) {
outlineWeight = zIndex = 2;
}
return {
strokeWeight: outlineWeight,
strokeColor: "#fff",
zIndex: zIndex,
fillColor: "hsl(" + color[0] + "," + color[1] + "%," + color[2] + "%)",
fillOpacity: 0.75,
visible: showRow,
};
}

/**
* Responds to the mouse-in event on a map shape (state).
*
* @param {?google.maps.MapMouseEvent} e
*/
function mouseInToRegion(e) {
// set the hover state so the setStyle function can change the border
e.feature.setProperty( "state", "hover" );

const percent =
((e.feature.getProperty( "census_variable" ) - censusMin) /
(censusMax - censusMin)) *
100;

// update the label
document.getElementById( "data-label" ).textContent =
e.feature.getProperty( "NAME" );
document.getElementById( "data-value" ).textContent = e.feature
.getProperty( "census_variable" )
.toLocaleString();
document.getElementById( "data-box" ).style.display = "block";
document.getElementById( "data-caret" ).style.display = "block";
document.getElementById( "data-caret" ).style.paddingLeft = percent + "%";
}

/**
* Responds to the mouse-out event on a map shape (state).
*
*/
function mouseOutOfRegion(e) {
// reset the hover state, returning the border to normal
e.feature.setProperty( "state", "normal" );
}

window.initMap = initMap;

CSS

html,
body,
#map {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}

.nicebox {
position: absolute;
text-align: center;
font-family: "Roboto", "Arial", sans-serif;
font-size: 13px;
z-index: 5;
box-shadow: 0 4px 6px -4px #333;
padding: 5px 10px;
background: rgb(255, 255, 255);
background: linear-gradient(to bottom, rgb(255, 255, 255) 0%, rgb(245, 245, 245) 100%);
border: rgb(229, 229, 229) 1px solid;
}

#controls {
top: 10px;
left: 110px;
width: 360px;
height: 45px;
}

#data-box {
top: 10px;
left: 500px;
height: 45px;
line-height: 45px;
display: none;
}

#census-variable {
width: 360px;
height: 20px;
}

#legend {
display: flex;
display: -webkit-box;
padding-top: 7px;
}

.color-key {
background: linear-gradient(to right, hsl(5, 69%, 54%) 0%, hsl(29, 71%, 51%) 17%, hsl(54, 74%, 47%) 33%, hsl(78, 76%, 44%) 50%, hsl(102, 78%, 41%) 67%, hsl(127, 81%, 37%) 83%, hsl(151, 83%, 34%) 100%);
flex: 1;
-webkit-box-flex: 1;
margin: 0 5px;
text-align: left;
font-size: 1em;
line-height: 1em;
}

#data-value {
font-size: 2em;
font-weight: bold;
}

#data-label {
font-size: 2em;
font-weight: normal;
padding-right: 10px;
}

#data-label:after {
content: ":";
}

#data-caret {
margin-left: -5px;
display: none;
font-size: 14px;
width: 14px;
}

HTML

<html>
<head>
<title>Mashups with google.maps.Data</title>

<link rel= "stylesheet" type= "text/css" href= "./style.css" />
<script type= "module" src= "./index.js" ></script>
</head>
<body>
<div id= "controls" class= "nicebox" >
<div>
<select id= "census-variable" >
<option
value= "https://storage.googleapis.com/mapsdevsite/json/DP02_0066PE"
>
Percent of population over 25 that completed high school
</option>
<option
value= "https://storage.googleapis.com/mapsdevsite/json/DP05_0017E"
>
Median age
</option>
<option
value= "https://storage.googleapis.com/mapsdevsite/json/DP05_0001E"
>
Total population
</option>
<option
value= "https://storage.googleapis.com/mapsdevsite/json/DP02_0016E"
>
Average family size
</option>
<option
value= "https://storage.googleapis.com/mapsdevsite/json/DP03_0088E"
>
Per-capita income
</option>
</select>
</div>
<div id= "legend" >
<div id= "census-min" >min</div>
<div class= "color-key" ><span id= "data-caret" >&#x25c6;</span></div>
<div id= "census-max" >max</div>
</div>
</div>
<div id= "data-box" class= "nicebox" >
<label id= "data-label" for= "data-value" ></label>
<span id= "data-value" ></span>
</div>
<div id= "map" ></div>

<!--
The `defer` attribute causes the script to execute after the full HTML
document has been parsed. For non-blocking uses, avoiding race conditions,
and consistent behavior across browsers, consider loading using Promises. See
https://developers.google.com/maps/documentation/javascript/load-maps-js-api
for more information.
-->
<script
src= "https://maps.googleapis.com/maps/api/js?key=AIzaSyB41DRUbKWJHPxaFjMAwdrzWzbVKartNGg&callback=initMap&v=weekly"
defer
></script>
</body>
</html>

Try Sample

Getting started

You can develop your own version of this choropleth map by using the code in this tutorial. To begin doing this, create a new file in a text editor and save it asindex.html.

Read the sections that follow to understand the code that you can add to this file.

Creating a basic map

This section explains the code that sets up a basic map. This may be similar to how you've created maps when getting started with the Maps JavaScript API.

Copy the code below into yourindex.htmlfile. This code loads the Maps JavaScript API, and makes the map fullscreen.

<!DOCTYPE html>
<html>
<head>
<meta charset= "utf-8" >
<meta name= "viewport" content= "initial-scale=1.0, user-scalable=no" />
<title>Mashups with google.maps.Data</title>
<style>
#map {
height: 100%;
}
/* Optional: Makes the sample page fill the window. */
html, body {
height: 100%;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id= "map" ></div>
<script>
function initMap() {

// load the map
map = new google.maps.Map(document.getElementById('map'), {
center: {lat: 40, lng: -100},
zoom: 4,
styles: mapStyle
});

var mapStyle = [{
'featureType': 'all',
'elementType': 'all',
'stylers': [{'visibility': 'off'}]
}, {
'featureType': 'landscape',
'elementType': 'geometry',
'stylers': [{'visibility': 'on'}, {'color': '#fcfcfc'}]
}, {
'featureType': 'water',
'elementType': 'labels',
'stylers': [{'visibility': 'off'}]
}, {
'featureType': 'water',
'elementType': 'geometry',
'stylers': [{'visibility': 'on'}, {'hue': '#5f94ff'}, {'lightness': 60}]
}];
}

</script>
<script defer
src= "https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap ">
</script>
</body>
</html>

The code within the first script tag is the starting point that runs the program by creating a function calledinitMapthat initializes the map object.

The stylers in the code above turn off the visibility of all featureTypeson the map like roads, points of interest, landscape, administrative areas, and all theirelementTypes.For a list of all available values for featureTypeandelementType,see the JSON style reference.

ClickYOUR_API_KEYin the code sample, or follow the instructions to get an API key. ReplaceYOUR_API_KEYwith your application's API key. After the API is completely loaded, the callback parameter in the script tag below executes the initMap()function in the HTML file.

<script> defer
src= "https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap "
</script>

Creating and styling the map control

The code below creates the following controls on the map:

  • A control with a dropdown menu that has 5 different data options.
  • A map legend.
  • A data box displaying state-specific data which appears when you hover over a polygon.
<div id= "controls" class= "nicebox" >
<div>
<select id= "census-variable" >
<option value= "https://storage.googleapis.com/mapsdevsite/json/DP02_0066PE" >Percent of population over 25 that completed high
school</option>
<option value= "https://storage.googleapis.com/mapsdevsite/json/DP05_0017E" >Median age</option>
<option value= "https://storage.googleapis.com/mapsdevsite/json/DP05_0001E" >Total population</option>
<option value= "https://storage.googleapis.com/mapsdevsite/json/DP02_0016E" >Average family size</option>
<option value= "https://storage.googleapis.com/mapsdevsite/json/DP03_0088E" >Per-capita income</option>
</select>
</div>
<div id= "legend" >
<div id= "census-min" >min</div>
<div class= "color-key" ><span id= "data-caret" >◆</span></div>
<div id= "census-max" >max</div>
</div>
</div>
<div id= "data-box" class= "nicebox" >
<label id= "data-label" for= "data-value" ></label>
<span id= "data-value" ></span>
</div>

Use the code below within thestyletags to style the map controls.

<style>
html, body, #map { height: 100%; margin: 0; padding: 0; overflow: hidden; }
.nicebox {
position: absolute;
text-align: center;
font-family: "Roboto", "Arial", sans-serif;
font-size: 13px;
z-index: 5;
box-shadow: 0 4px 6px -4px #333;
padding: 5px 10px;
background: rgb(255,255,255);
background: linear-gradient(to bottom,rgba(255,255,255,1) 0%,rgba(245,245,245,1) 100%);
border: rgb(229, 229, 229) 1px solid;
}
#controls {
top: 10px;
left: 110px;
width: 360px;
height: 45px;
}
#data-box {
top: 10px;
left: 500px;
height: 45px;
line-height: 45px;
display: none;
}
#census-variable {
width: 360px;
height: 20px;
}
#legend { display: flex; display: -webkit-box; padding-top: 7px }
.color-key {
background: linear-gradient(to right,
hsl(5, 69%, 54%) 0%,
hsl(29, 71%, 51%) 17%,
hsl(54, 74%, 47%) 33%,
hsl(78, 76%, 44%) 50%,
hsl(102, 78%, 41%) 67%,
hsl(127, 81%, 37%) 83%,
hsl(151, 83%, 34%) 100%);
flex: 1;
-webkit-box-flex: 1;
margin: 0 5px;
text-align: left;
font-size: 1.0em;
line-height: 1.0em;
}
#data-value { font-size: 2.0em; font-weight: bold }
#data-label { font-size: 2.0em; font-weight: normal; padding-right: 10px; }
#data-label:after { content: ':' }
#data-caret { margin-left: -5px; display: none; font-size: 14px; width: 14px}
</style>

Importing data from the US Census API

The code below queries the US Census Bureau for the most recent census data of all US states, which it receives in JSON format.

function loadCensusData(variable) {
// load the requested variable from the census API
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://api.census.gov/data/2012/acs5/profile?get=' +
variable + '&for=state:*&key=YOUR_API_KEY');
xhr.onload = function() {
var censusData = JSON.parse(xhr.responseText);
censusData.shift(); // the first row contains column names
censusData.forEach(function(row) {
var censusVariable = parseFloat(row[0]);
var stateId = row[1];

// keep track of min and max values
if (censusVariable < censusMin) {
censusMin = censusVariable;
}
if (censusVariable > censusMax) {
censusMax = censusVariable;
}

// update the existing row with the new data
map.data
.getFeatureById(stateId)
.setProperty('census_variable', censusVariable);
});

// update and display the legend
document.getElementById('census-min').textContent =
censusMin.toLocaleString();
document.getElementById('census-max').textContent =
censusMax.toLocaleString();
};
xhr.send();
}

Styling the data

The code below creates the choropleth map by applying a gradient to each polygon in the dataset, based on the census data value. You can style data using a Data.StyleOptions object, or a function that returns aData.StyleOptionsobject.

// set up the style rules and events for google.maps.Data
map.data.setStyle(styleFeature);

function styleFeature(feature) {
var low = [5, 69, 54]; // color of smallest datum
var high = [151, 83, 34]; // color of largest datum

// delta represents where the value sits between the min and max
var delta = (feature.getProperty('census_variable') - censusMin) /
(censusMax - censusMin);

var color = [];
for (var i = 0; i < 3; i++) {
// calculate an integer color based on the delta
color[i] = (high[i] - low[i]) * delta + low[i];
}

// determine whether to show this shape or not
var showRow = true;
if (feature.getProperty('census_variable') == null ||
isNaN(feature.getProperty('census_variable'))) {
showRow = false;
}

var outlineWeight = 0.5, zIndex = 1;
if (feature.getProperty('state') === 'hover') {
outlineWeight = zIndex = 2;
}

return {
strokeWeight: outlineWeight,
strokeColor: '#fff',
zIndex: zIndex,
fillColor: 'hsl(' + color[0] + ',' + color[1] + '%,' + color[2] + '%)',
fillOpacity: 0.75,
visible: showRow
};
}

In addition to coloring the polygons, the code below creates an interactive element by adding events that respond to mouse activity. Hovering over a polygon highlights the state border, and simultaneously updates the data box control on the map.

// set up the style rules and events for google.maps.Data
map.data.addListener('mouseover', mouseInToRegion);
map.data.addListener('mouseout', mouseOutOfRegion);

/**
* Responds to the mouse-in event on a map shape (state).
*
* @param {?google.maps.MapMouseEvent} e
*/
function mouseInToRegion(e) {
// set the hover state so the setStyle function can change the border
e.feature.setProperty('state', 'hover');

var percent = (e.feature.getProperty('census_variable') - censusMin) /
(censusMax - censusMin) * 100;

// update the label
document.getElementById('data-label').textContent =
e.feature.getProperty('NAME');
document.getElementById('data-value').textContent =
e.feature.getProperty('census_variable').toLocaleString();
document.getElementById('data-box').style.display = 'block';
document.getElementById('data-caret').style.display = 'block';
document.getElementById('data-caret').style.paddingLeft = percent + '%';
}

/**
* Responds to the mouse-out event on a map shape (state).
*
* @param {?google.maps.MapMouseEvent} e
*/
function mouseOutOfRegion(e) {
// reset the hover state, returning the border to normal
e.feature.setProperty('state', 'normal');
}

Loading the state-boundary polygons

Add the code below after the entireinitMapfunction. TheloadMapShapes function loads polygons for US state boundaries from a GeoJSON file, using the loadGeoJsonmethod.

/** Loads the state boundary polygons from a GeoJSON source. */
function loadMapShapes() {
// load US state outline polygons from a GeoJSON file
map.data.loadGeoJson('https://storage.googleapis.com/mapsdevsite/json/states.js', { idPropertyName: 'STATE' });

Add the line below to the end of theinitMapfunction.

// state polygons only need to be loaded once, do them now
loadMapShapes();

On selecting a data source option from the map control dropdown menu, the map queries the US Census Data APIfor the specified variable. To connect the census data with the shape data, the code sets the idPropertyName to 'STATE', which is a common key in both the Census data and in the GeoJson properties.

More information

This demo uses theCensus Bureau Data API,but is not endorsed or certified by the Census Bureau.