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