Google Maps and deck.gl HeatmapLayer

Visualize data with a deck.gl heatmap layer

This example uses uses the deck.gl HeatmapLayer to show bicycle parking capacity. Use the HeatmapLayer to visualize spatial distribution of data.

TypeScript

// Initialize and add the map
let map: google.maps.Map;
// Use global types for Deck.gl components
let heatmapLayer: deck.HeatmapLayer;
let googleMapsOverlay: deck.GoogleMapsOverlay;
let marker: google.maps.marker.AdvancedMarkerElement | undefined;
let infoWindow: google.maps.InfoWindow;

// Declare global namespace for Deck.gl to satisfy TypeScript compiler
declare namespace deck {
  class HeatmapLayer {
    constructor(props: any);
    props: any;
    clone(props: any): HeatmapLayer;
    pickable: boolean; // Added pickable property
  }
  class GoogleMapsOverlay {
    constructor(props: any);
    setMap(map: google.maps.Map | null): void;
    setProps(props: any): void;
  }
  // Add other Deck.gl types used globally if needed
}

async function initMap(): Promise<void> {
  // Progress bar logic moved from index.html
  var progress, progressDiv = document.querySelector(".mdc-linear-progress");
  if (progressDiv) {
    // Assuming 'mdc' is globally available, potentially loaded via a script tag
    // If not, you might need to import it or add type definitions.
    // @ts-ignore
    progress = new mdc.linearProgress.MDCLinearProgress(progressDiv as HTMLElement);
    progress.open();
    progress.determinate = false;
    progress.done = function () {
      progress.close();
      progressDiv?.remove(); // Use optional chaining in case progressDiv is null
    };
  }

  // The location for the map center.
  const position = {lat:37.77325660358167, lng:-122.41712341793448}; // Using the center from original deckgl-polygon.js

  //  Request needed libraries.
  const {Map, InfoWindow} =
      await google.maps.importLibrary('maps') as google.maps.MapsLibrary;
  const {AdvancedMarkerElement} = await google.maps.importLibrary('marker') as google.maps.MarkerLibrary;

  const mapDiv = document.getElementById('map');
  if (!mapDiv) {
    console.error('Map element not found!');
    return;
  }

  // The map, centered at the specified position
  map = new Map(mapDiv, {
    zoom: 13, // Using the zoom from original deckgl-polygon.js
    center: position,
    tilt: 90, // Add tilt
    heading: -25, // Add heading
    mapId: '6b73a9fe7e831a00',
    fullscreenControl: false, // Disable fullscreen control
    clickableIcons: false, // Disable clicks on base map POIs
  });

  // Deck.gl Layer and Overlay
  // Use global deck object
  heatmapLayer = new deck.HeatmapLayer({ // Assign to the outer heatmapLayer
      id: 'HeatmapLayer', // Change layer ID
      data: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/sf-bike-parking.json', // Use the loaded data
      getPosition: (d: any) => d.COORDINATES, // Use 'any' for simplicity, or define a proper type
      getWeight: (d: any) => d.SPACES, // Use 'any' for simplicity, or define a proper type
      radiusPixels: 25, // Adjust radius as in user's example

      visible: true,
      pickable: true,
      onHover: (info: any) => { // Use 'any' for info for simplicity, or define a proper type
        const tooltip = document.getElementById('tooltip');
        if (tooltip) {
          console.log('Hovered object:', info.object);
          if (info.object) {
            // Format data for tooltip (display ADDRESS, RACKS, SPACES)
            let tooltipContent = '<h4>Bike Parking Info:</h4>'; // Updated title
            if (info.object.ADDRESS !== undefined) {
              tooltipContent += `<strong>Address:</strong> ${info.object.ADDRESS}<br>`;
            }
            if (info.object.RACKS !== undefined) {
              tooltipContent += `<strong>Racks:</strong> ${info.object.RACKS}<br>`;
            }
            if (info.object.SPACES !== undefined) {
              tooltipContent += `<strong>Spaces:</strong> ${info.object.SPACES}<br>`;
            }
            tooltip.innerHTML = tooltipContent;
            tooltip.style.left = info.x + 'px';
            tooltip.style.top = info.y + 'px';
            tooltip.style.display = 'block';
          } else {
            tooltip.style.display = 'none';
          }
          console.log('Tooltip content set to:', tooltip.innerHTML);
        }
      }
    });

    heatmapLayer.pickable = true; // Ensure pickable is true after creation

    // Use global deck object
    googleMapsOverlay = new deck.GoogleMapsOverlay({ // Assign to the outer googleMapsOverlay
      layers: [heatmapLayer],
      controller: true // Enable Deck.gl to control map view
    });

    googleMapsOverlay.setMap(map);

    // Hide progress bar after data is loaded and layer is added
    if (progress) { // Check if progress is defined
      // Add a small delay to ensure the progress bar is removed
      setTimeout(() => {
        // @ts-ignore
        progress.done(); // hides progress bar
      }, 100); // 100ms delay
    }

    // Create a single InfoWindow instance
    infoWindow = new InfoWindow();

    // Add click listener to the map
    map.addListener('click', async (event: google.maps.MapMouseEvent) => {
      const latLng = event.latLng;
      if (!latLng) return; // Ensure latLng is not null

      if (!marker) {
        // Create the marker on the first click
        marker = new AdvancedMarkerElement({
          map: map,
          position: latLng,
          gmpClickable: true,
        });

        // Add click listener to the marker
        marker.addListener("click", () => {
          infoWindow.close();
          const content = `
            <div>Location: ${latLng.lat().toFixed(3)}, ${latLng.lng().toFixed(3)}</div>
            <div><a href="https://www.google.com/maps/search/?api=1&query=${latLng.lat()},${latLng.lng()}" target="_blank">Open in Google Maps</a></div>
          `;
          infoWindow.setContent(content);
          infoWindow.open(map, marker);
        });

        // Open InfoWindow immediately on first click
        const content = `
          <div>Location: ${latLng.lat().toFixed(3)}, ${latLng.lng().toFixed(3)}</div>
          <div><a href="https://www.google.com/maps/search/?api=1&query=${latLng.lat()},${latLng.lng()}" target="_blank">Open in Google Maps</a></div>
        `;
        infoWindow.setContent(content);
        infoWindow.open(map, marker);


      } else {
        // Move the existing marker on subsequent clicks
        marker.position = latLng;
        // InfoWindow remains open
        const content = `
          <div>Location: ${latLng.lat().toFixed(3)}, ${latLng.lng().toFixed(3)}</div>
          <div><a href="https://www.google.com/maps/search/?api=1&query=${latLng.lat()},${latLng.lng()}" target="_blank">Open in Google Maps</a></div>
        `;
        infoWindow.setContent(content);
        infoWindow.open(map, marker);
      }
    });


  // Button functionality
  const toggleButton = document.getElementById('toggleButton');
  if (toggleButton) { // Check if toggleButton is found
    toggleButton.addEventListener('click', () => {
      const currentVisible = heatmapLayer.props.visible;
      // Create a new layer instance with toggled visibility and update the overlay
      const newLayer = heatmapLayer.clone({ visible: !currentVisible });
      googleMapsOverlay.setProps({
        layers: [newLayer]
      });
      heatmapLayer = newLayer; // Update the heatmapLayer variable

      toggleButton.textContent = !currentVisible ? 'Hide Heatmap Layer' : 'Show Heatmap Layer';
    });
  }
}

initMap();

JavaScript

// Initialize and add the map
let map;
// Use global types for Deck.gl components
let heatmapLayer;
let googleMapsOverlay;
let marker;
let infoWindow;
async function initMap() {
    // Progress bar logic moved from index.html
    var progress, progressDiv = document.querySelector(".mdc-linear-progress");
    if (progressDiv) {
        // Assuming 'mdc' is globally available, potentially loaded via a script tag
        // If not, you might need to import it or add type definitions.
        // @ts-ignore
        progress = new mdc.linearProgress.MDCLinearProgress(progressDiv);
        progress.open();
        progress.determinate = false;
        progress.done = function () {
            progress.close();
            progressDiv?.remove(); // Use optional chaining in case progressDiv is null
        };
    }
    // The location for the map center.
    const position = { lat: 37.77325660358167, lng: -122.41712341793448 }; // Using the center from original deckgl-polygon.js
    //  Request needed libraries.
    const { Map, InfoWindow } = await google.maps.importLibrary('maps');
    const { AdvancedMarkerElement } = await google.maps.importLibrary('marker');
    const mapDiv = document.getElementById('map');
    if (!mapDiv) {
        console.error('Map element not found!');
        return;
    }
    // The map, centered at the specified position
    map = new Map(mapDiv, {
        zoom: 13, // Using the zoom from original deckgl-polygon.js
        center: position,
        tilt: 90, // Add tilt
        heading: -25, // Add heading
        mapId: '6b73a9fe7e831a00',
        fullscreenControl: false, // Disable fullscreen control
        clickableIcons: false, // Disable clicks on base map POIs
    });
    // Deck.gl Layer and Overlay
    // Use global deck object
    heatmapLayer = new deck.HeatmapLayer({
        id: 'HeatmapLayer', // Change layer ID
        data: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/sf-bike-parking.json', // Use the loaded data
        getPosition: (d) => d.COORDINATES, // Use 'any' for simplicity, or define a proper type
        getWeight: (d) => d.SPACES, // Use 'any' for simplicity, or define a proper type
        radiusPixels: 25, // Adjust radius as in user's example
        visible: true,
        pickable: true,
        onHover: (info) => {
            const tooltip = document.getElementById('tooltip');
            if (tooltip) {
                console.log('Hovered object:', info.object);
                if (info.object) {
                    // Format data for tooltip (display ADDRESS, RACKS, SPACES)
                    let tooltipContent = '<h4>Bike Parking Info:</h4>'; // Updated title
                    if (info.object.ADDRESS !== undefined) {
                        tooltipContent += `<strong>Address:</strong> ${info.object.ADDRESS}<br>`;
                    }
                    if (info.object.RACKS !== undefined) {
                        tooltipContent += `<strong>Racks:</strong> ${info.object.RACKS}<br>`;
                    }
                    if (info.object.SPACES !== undefined) {
                        tooltipContent += `<strong>Spaces:</strong> ${info.object.SPACES}<br>`;
                    }
                    tooltip.innerHTML = tooltipContent;
                    tooltip.style.left = info.x + 'px';
                    tooltip.style.top = info.y + 'px';
                    tooltip.style.display = 'block';
                }
                else {
                    tooltip.style.display = 'none';
                }
                console.log('Tooltip content set to:', tooltip.innerHTML);
            }
        }
    });
    heatmapLayer.pickable = true; // Ensure pickable is true after creation
    // Use global deck object
    googleMapsOverlay = new deck.GoogleMapsOverlay({
        layers: [heatmapLayer],
        controller: true // Enable Deck.gl to control map view
    });
    googleMapsOverlay.setMap(map);
    // Hide progress bar after data is loaded and layer is added
    if (progress) { // Check if progress is defined
        // Add a small delay to ensure the progress bar is removed
        setTimeout(() => {
            // @ts-ignore
            progress.done(); // hides progress bar
        }, 100); // 100ms delay
    }
    // Create a single InfoWindow instance
    infoWindow = new InfoWindow();
    // Add click listener to the map
    map.addListener('click', async (event) => {
        const latLng = event.latLng;
        if (!latLng)
            return; // Ensure latLng is not null
        if (!marker) {
            // Create the marker on the first click
            marker = new AdvancedMarkerElement({
                map: map,
                position: latLng,
                gmpClickable: true,
            });
            // Add click listener to the marker
            marker.addListener("click", () => {
                infoWindow.close();
                const content = `
            <div>Location: ${latLng.lat().toFixed(3)}, ${latLng.lng().toFixed(3)}</div>
            <div><a href="https://www.google.com/maps/search/?api=1&query=${latLng.lat()},${latLng.lng()}" target="_blank">Open in Google Maps</a></div>
          `;
                infoWindow.setContent(content);
                infoWindow.open(map, marker);
            });
            // Open InfoWindow immediately on first click
            const content = `
          <div>Location: ${latLng.lat().toFixed(3)}, ${latLng.lng().toFixed(3)}</div>
          <div><a href="https://www.google.com/maps/search/?api=1&query=${latLng.lat()},${latLng.lng()}" target="_blank">Open in Google Maps</a></div>
        `;
            infoWindow.setContent(content);
            infoWindow.open(map, marker);
        }
        else {
            // Move the existing marker on subsequent clicks
            marker.position = latLng;
            // InfoWindow remains open
            const content = `
          <div>Location: ${latLng.lat().toFixed(3)}, ${latLng.lng().toFixed(3)}</div>
          <div><a href="https://www.google.com/maps/search/?api=1&query=${latLng.lat()},${latLng.lng()}" target="_blank">Open in Google Maps</a></div>
        `;
            infoWindow.setContent(content);
            infoWindow.open(map, marker);
        }
    });
    // Button functionality
    const toggleButton = document.getElementById('toggleButton');
    if (toggleButton) { // Check if toggleButton is found
        toggleButton.addEventListener('click', () => {
            const currentVisible = heatmapLayer.props.visible;
            // Create a new layer instance with toggled visibility and update the overlay
            const newLayer = heatmapLayer.clone({ visible: !currentVisible });
            googleMapsOverlay.setProps({
                layers: [newLayer]
            });
            heatmapLayer = newLayer; // Update the heatmapLayer variable
            toggleButton.textContent = !currentVisible ? 'Hide Heatmap Layer' : 'Show Heatmap Layer';
        });
    }
}
initMap();

CSS

/*
 * Always set the map height explicitly to define the size of the div element
 * that contains the map.
 */
 #map {
  height: 100%;
  flex-grow: 1; /* Make map take up remaining space */
}

/*
 * Optional: Makes the sample page fill the window.
 */
html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
  display: flex; /* Use flexbox for layout */
  flex-direction: column; /* Stack children vertically */
  position: relative; /* Set body as positioning context */
  font-family: 'Roboto', Arial, sans-serif; /* Set font family */
}

#toggleButton {
position: absolute;
top: 70px; /* Position towards the top of the map area */
left: 50%;
transform: translateX(-50%);
z-index: 1000; /* Ensure it's above the map */
}

#tooltip {
position: absolute;
z-index: 1001; /* Ensure it's above the button and map */
padding: 10px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 4px;
pointer-events: none; /* Allows interaction with elements behind the tooltip */
display: none; /* Hidden by default */
font-size: 126x;
}

h1 {
text-align: center;
margin:10px;
}

#legend {
  position: absolute;
  top: 90px; /* Adjust position as needed */
  right: 10px; /* Position on the left */
  z-index: 1000; /* Ensure it's above the map */
  background-color: rgba(255, 255, 255, 0.9);
  padding: 10px;
  border-radius: 4px;
  font-size: 14px;
}

#legend h4 {
  margin-top: 0;
  margin-bottom: 5px;
}

#legend div {
  display: flex;
  align-items: center;
  margin-bottom: 3px;
}

#legend span {
  display: inline-block;
  width: 20px;
  height: 14px;
  margin-right: 5px;
  border: 1px solid #000; /* Add a border for visibility */
}

/* Media query for mobile devices */
@media (max-width: 600px) {
  #legend {
    position: absolute;
    bottom: 50px; /* Position above the button */
    left: 50%; /* Center horizontally */
    transform: translateX(-50%); /* Adjust for centering */
    top: auto; /* Remove top positioning */
    right: auto; /* Remove right positioning */
  }

  #toggleButton {
    position: absolute;
    bottom: 10px; /* Position at the bottom */
    left: 50%; /* Center horizontally */
    transform: translateX(-50%); /* Adjust for centering */
    top: auto; /* Remove top positioning */
  }
}

HTML

<html>
  <head>
    <title>deck.gl HeatmapLayer and Google Maps Platform</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <!-- Use Material Design Progress indicator -->
    <link
      href="https://unpkg.com/material-components-web@6.0.0/dist/material-components-web.css"
      rel="stylesheet"
    />
    <script src="https://unpkg.com/material-components-web@6.0.0/dist/material-components-web.min.js"></script>
    <script src="https://unpkg.com/deck.gl@8.9.22/dist.min.js"></script>
    <script src="https://unpkg.com/@deck.gl/google-maps@8.9.22/dist.min.js"></script>

    <link rel="stylesheet" href="style.css">

    <script type="module" src="index.js" defer></script>
  </head>
  <body>
   <div
   role="progressbar"
   class="mdc-linear-progress"
   aria-label="Data Progress Bar"
 >
   <div class="mdc-linear-progress__buffer">
     <div class="mdc-linear-progress__buffer-bar"></div>
     <div class="mdc-linear-progress__buffer-dots"></div>
   </div>
   <div class="mdc-linear-progress__bar mdc-linear-progress__primary-bar">
     <span class="mdc-linear-progress__bar-inner"></span>
   </div>
   <div class="mdc-linear-progress__bar mdc-linear-progress__secondary-bar">
     <span class="mdc-linear-progress__bar-inner"></span>
   </div>
 </div>
 <script>
   var progress, progressDiv;
   progressDiv = document.querySelector(".mdc-linear-progress");
   progress = new mdc.linearProgress.MDCLinearProgress(progressDiv);
   progress.open();
   progress.determinate = false;
   progress.done = function () {
     progress.close();
     progressDiv.remove();
   };
 </script>

    <h1>Bike Parking Heatmap</h1>

    <div id="map"></div>

    <div id="legend">
      <h4>Bike Parking Spaces</h4>
      <div><span style="background-color: #ffffe5;"></span>0 - 33</div>
      <div><span style="background-color: #ffffb2;"></span>34 - 66</div>
      <div><span style="background-color: #fecc5c;"></span>67 - 100</div>
      <div><span style="background-color: #fd8d3c;"></span>101 - 133</div>
      <div><span style="background-color: #f03b20;"></span>134 - 166</div>
      <div><span style="background-color: #bd0026;"></span>167+</div>
    </div>

    <button id="toggleButton">Hide Heatmap Layer</button>

    <script>(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})
        ({key: "AIzaSyA6myHzS10YXdcazAFalmXvDkrYCp5cLc8", v: "weekly"});</script>

  </body>
</html>

Try Sample

Clone Sample

Git and Node.js are required to run this sample locally. Follow these instructions to install Node.js and NPM. The following commands clone, install dependencies and start the sample application.

  git clone https://github.com/googlemaps-samples/js-api-samples.git
  cd samples/deckgl-heatmap
  npm i
  npm start