Heat Maps in Windows Store Apps (JavaScript)

Heat maps are an effective means of visualizing geography-based trends by showing the relative density of location-based data. We can see where we have ‘hot spots’ in our data, and use this insight to drive better decisions for application users. In this blog post, we will show how you can visualize location data in the form of a heat map within Windows Store apps, pulling in both public points-of-interest data sources available within Bing Maps, as well as custom data sources containing your own data.

We will use the Bing Maps Windows Store JavaScript API and a Heat Map Module to create our visualization, and we will pull in our data via the Bing Maps Spatial Data Query API. The full source code for the application is available in the Visual Studio Samples Gallery.

To run this application, you must install the Bing Maps SDK for Windows Store apps and get a Bing Maps key for Windows Store apps. You must also have Windows 8 and Visual Studio 2012.

Instantiating Our Map

We will create a JavaScript file for our project in which all of our custom JavaScript code for the app will reside. Within this file, we will include initialization logic to populate a dropdown with available data sources, load the Map module and use the loadMap callback to instantiate our map:

//Initialization logic for populating  data source options and loading the map control 
   (function () {
   function initialize() {
   loadDataSourceOptions(publicdatasources);
   Microsoft.Maps.loadModule('Microsoft.Maps.Map', { callback: loadMap });
   }
    document.addEventListener("DOMContentLoaded", initialize, false);
})(); 

The loadMap callback will include the code to instantiate our map, and to add event handlers to manage the retrieval of the appropriate data from our data source as the user navigates the map. We will also load the heat map module which provides the heat map rendering capabilities:

// Called once app is loaded or map is  reloaded, instantiating the map and initiating the loading of data: 
function loadMap() {
   
 …
   // Load the map: 
   map = new MM.Map(document.getElementById("mapDiv"),
   {
   credentials:  document.getElementById("txtQueryKey").value,
   mapTypeId:  startMaptype,
   zoom: startZoom,
   center: startCenter
 });
…   // Load the Client Side  HeatMap Module 
   Microsoft.Maps.loadModule("HeatMapModule", {
   callback: function () {
   }
   });
} 

The heat map module was created by Alastair Aitchison, Microsoft MVP, and is shared on CodePlex as part of the Bing Maps V7 Modules project.

Accessing the Data

We will be drawing on data hosted in data sources within Bing Maps Spatial Data Services. Bing Maps includes Public Data Sources, including:

  • FourthCoffeeSample – Sample data for a fictional coffee chain
  • NavteqNA – Points of Interest for North America, categorized by SIC
  • NavteqEU – Points of Interest for Europe, categorized by SIC
  • Traffic Incident Data Source – Traffic incidents

We will also enable access to private data sources that you can create and populate with the Spatial Data Services Data Source Management API.

As the user navigates the map to a new view, we will dynamically load only the data from the data source that is located within the current map view, for performance reasons. This allows us to work with data sources that contain millions of entities, while still ensuring reasonable performance. We will add a throttled event handler for the viewchangeend event to retrieve the data, and an event handler for the viewchangestart event to cancel any outstanding data retrieval processes if the map is navigated again before they complete:

// Add Event to search for entities when  map moved: 
   Microsoft.Maps.Events.addThrottledHandler(map, 'viewchangeend', NavEndHandler, 2000);
    // Add Event to check whether  to cancel the retrieval of entity data if the map is 
moved before data is  returned: 
   Microsoft.Maps.Events.addHandler(map, 'viewchangestart', CheckSearch);

The user is given the option to opt-out of reloading data as the map is navigated, so the NavEventHandler function will confirm that data should be reloaded, and then initiate the search. The InitiateSearch function will remove any existing heat map layer, reset various values, and elements of the UI, and then retrieve the credentials for the map session, to use in a request to the SDS Query API in the ExecuteSearch function:

function NavEndHandler() {
  // Reload data, unless user  has checked otherwise: 
if (document.getElementById("chkUpdate").checked) { InitiateSearch(); }
   }
function InitiateSearch() {
    // remove heatmap, if  present: 
if (heatmapLayer) { removeHeatmapLayer() };
    // reset search results  array, and pagination global variables: 
   searchResultsArray = new Array();
   searchResultsPage = 0;
   document.getElementById("spanZoomWarning").style.display = "none";
   document.getElementById("spanNumEntities").innerHTML = "...";
    // update progress bar: 
var progressBar = document.getElementById("determinateProgressBar");
   progressBar.value = 0;
    // retrieve map credentials,  and use them to execute a search against the data 
source 
   map.getCredentials(ExecuteSearch);
}

The ExecuteSearch function will obtain the current map view, and determine the bounds of the map for use in a Query by Area (bounding-box) with the Query API. Note that we also include additional query options and parameters, including:

  • $top=250 – this is the maximum number of records we can retrieve in any single request
  • $skip – we use this parameter to retrieve successive batches of results, if there are more than 250
  • $select=Latitude,Longitude – we want to minimize the amount of data transferred, and these two attributes are the minimum required to create our heat map
  • $filter – the UI includes a freeform text input to allow filter text to be specified. This would not typically be exposed in a production-ready app, but it gives us flexibility to easily work with any supported filters
  • $inlinecount=allpages – this will cause the total number of entities that sit within the current map view and that meet our filter criteria to be included in the response. This will allow us to make a decision as to whether to continue requesting all of the data and rendering it on our map, depending on a threshold that the user will set in the UI

The request is executed using the WinJS.xhr function, with completed and error callbacks specified:

function ExecuteSearch(credentials) {
   // get map bounds for  filtering: 
   var bounds = map.getBounds();
   var south = bounds.getSouth();
   var north = bounds.getNorth();
   var east = bounds.getEast();
   var west = bounds.getWest();
    // Construct query URL: 
   var searchRequest = "http://spatial.virtualearth.net/REST/v1/data/" + 
document.getElementById("txtDSID").value + "/" + 
document.getElementById("txtDSName").value + "/" + document.getElementById("txtEntityName").value;
     // Add filter clauses, if  appropriate: 
   var searchFilter = (document.getElementById("txtFilter").value != "") ? "$filter=" 
+ document.getElementById("txtFilter").value + "&" : "";
   searchRequest += "?spatialFilter=bbox(" + south + "," + west + "," + north + "," + 
east + ")&" + searchFilter + "$inlinecount=allpages&$select=Latitude,Longitude&$format=JSON&key=" + credentials;
   searchRequest += "&$top=250&$skip=" + (searchResultsPage * 250).toString();
    // Use WinJS to execute  request: 
   xhrPromise = WinJS.xhr({ url: searchRequest }).then(SearchCallback,  ErrorCallback);
   searchResultsPage++;
} 

Our SearchCallback function is called when the request completes successfully. This function will make sure that the total number of entities that match our search criteria does not exceed the maximum number the user specifies in the UI. If not, it will add the results to an array, update a progress bar, and then check to see if we still need to retrieve more data. If so, an additional request is issued. If not, the results are turned into an array of Microsoft.Map.Location objects, and the drawHeatmap function is called:

function SearchCallback(result) {
   xhrPromise = null;
   // Parse results: 
   result = JSON.parse(result.responseText);
   if (result &&
   		result.d &&
   		result.d.results &&
   		result.d.results.length > 0)  {
        if (result.d.__count >  document.getElementById("txtLimit").value) {
            // Show warning: 
   	     document.getElementById("spanZoomWarning").style.display = "block";
            // Update span to show total entities: 
   	     document.getElementById("spanNumEntities").innerHTML =
result.d.__count.toString();
   		return;
   	}
        // grab search results from  response 
   	  searchResultsArray = searchResultsArray.concat(result.d.results);
        // Update span to show total  entities: 
   	  document.getElementById("spanNumEntities").innerHTML =
 result.d.__count.toString();
         // Update progress bar: 
         var progressBar = document.getElementById("determinateProgressBar");
   	  progressBar.max = result.d.__count;
   	  progressBar.value = searchResultsArray.length;
        // check to see if we need to  retrieve more results: 
   	  if (result.d.__count >  (searchResultsPage * 250)) {
   	  map.getCredentials(ExecuteSearch);
	 } else {
   	    // Process results: 
   	   // grab search results from response 
   	   var locations = result.d.results;
       // Array to use for heatmap: 
   	  heatmapLocations = new Array();
       for (var j = 0; j < searchResultsArray.length; j++) {
   	      var  location = new  MM.Location(searchResultsArray[j].Latitude, 
searchResultsArray[j].Longitude);
   		heatmapLocations.push(location);
  	    }
       drawHeatmap();
       }
  } else {
   	// update entities count: 
   	document.getElementById("spanNumEntities").innerHTML = "0";
  }
}

Note that other methodologies of retrieving data for heat mapping would be possible in the cases of custom Bing Maps data sources, or other sources of location data, including batch retrieval of data for local storage and rendering.

In the drawHeatmap function, we remove any existing heat map, and then create a new heat map layer using our array of locations, and using rendering options the user has specified in the UI, which will affect the radius and intensity of the hotspots for each location:

// redraw the heatmap: 
function drawHeatmap() {
   // remove heatmap, if  present: 
   if (heatmapLayer) { removeHeatmapLayer() };
   // Construct heatmap layer, using heatmapping JS module: 
   heatmapLayer = new HeatMapLayer(
       map,
       [],
       {
   		intensity: document.getElementById("txtIntensity").value,
   		radius: document.getElementById("txtRadius").value,
   		colourgradient: {
   			0.0: 'blue',
   			0.25: 'green',
   			0.5: 'yellow',
   			0.75: 'orange',
   			1.0: 'red' 
   		}
   	});
   	heatmapLayer.SetPoints(heatmapLocations);
}  

Running the Application

If you have downloaded the source code for the application, you can simply open the sample in Visual Studio and insert your Bing Maps key in the default.html file where it says "INSERT_BING_MAPS_KEY_HERE" in the source code. Build the sample by pressing F5. 

Here is a screenshot of the application, showing hotspots of restaurants in Seattle:

DataSourceHeatmap 

Some tips on using the app:

  • You can manually choose from four publicly available data sources (FourthCoffee, NavteqNA, NavteqEU, or TrafficIncidents) or enter your own data source details and Bing Maps key, to reload the map and view your own custom data. You will need to enter a key that has permissions to query private data sources
  • The app uses the current map view to retrieve only those entities in the current view
    • As the map is navigated, the entities in the current map view will be reloaded, unless the ‘Update entities on pan/zoom?’ checkbox is de-selected. De-selecting the checkbox can be useful for analyzing a particular heat map view without the performance hit of reloading all of the data
  • You can add in your own filter text, to apply filters on-the-fly, though you must know the data source schema to use this effectively
  • You can select your own Maximum Entities value, to choose your own cut-off point in the tradeoff between performance and data volume
  • As you navigate the map, you can update the heat map radius and intensity, to best show the current data, based on zoom factor and data concentration
    • If you click the ‘Update Heat’ button, it will update these attributes without reloading all of the data
  • If you change the Maximum Entities or Filter text, and wish to reload the data, or if you have deselected the ‘Update entities on pan/zoom?’ checkbox, you can reload the data on demand with the ‘Reload data’ button

Scenarios

Both consumer- and enterprise-oriented apps can benefit from the visualization of data in this way. Some possible scenarios include the following:

  • Heat maps of restaurants/bars in a city to show where nightlife is concentrated
  • Heat maps showing where specific industry is concentrated, for civic planning, event planning, or site selection
  • Heat maps showing where competing retail chains are concentrated, for business intelligence or site selection
  • Heat maps of your customers, opportunities, or service requests from enterprise data in Dynamics CRM, to drive smarter planning and operational optimization business decisions

While this example application draws on data from Bing Maps public and private data sources, the same concepts can be leveraged to present compelling visualizations of data from SQL Server, SQL Azure, Dynamics, and other sources of location-based data.

Geoff Innis
Bing Maps Technical Specialist