Search code examples
javascriptgoogle-mapsgoogle-maps-api-3markerclusterergooglemapsjs-markerclusterer

In the Google Maps API V3 MarkerClusterer class, how can I create multiple styles for MarkerClusters based on zoom level?


In the old @google/markercluster class, you could pass in a styles variable, which could contain multiple cluster styles which could then be passed into a markerCluster constructor, like below.

var clusterStyles = [
    {
         textColor: 'white',
         url: '../Images/MapIcons/m1.png',
         height: 53,
         width: 53,
    },
    {
        textColor: 'white',
        url: '../Images/MapIcons/m2.png',
        height: 57,
        width: 57,
    },
    {
        textColor: 'white',
        url: '../Images/MapIcons/m3.png',
        height: 66,
        width: 66,
    },             
    {
        textColor: 'white',
        url: '../Images/MapIcons/m4.png',
        height: 78,
        width: 78,
    },
    {
        textColor: 'white',
        url: '../Images/MapIcons/m5.png',
        height: 89,
        width: 89,
    }

markerCluster = new MarkerClusterer(map, markers,mcOptions);

The cluster would then handle zoom levels by changing which icon it was using, as well as the size of the icon in question. The icon would shrink as it got further zoomed in so that they didn't overlap.

However, the new implementation of MarkerClusterer replaces this with a Renderer object, which as far as I can tell, only allows a single definition, not multiple, so it won't handle zooming in.

let renderer = {
    render: ({ count, position }) =>
        new google.maps.Marker({
            label: {
                text: String(count),
                color: "white",
            },
            position,
            icon: {
                url: '../Images/MapIcons/m5.png',
                height: 89,
                width: 89,
            },

            zIndex: Number(google.maps.Marker.MAX_ZINDEX) + count,
        }
    ),
};
let algorithm = new markerClusterer.SuperClusterAlgorithm({ maxZoom: 15 });

map.setOptions({ minZoom: 5, maxZoom: 20 });
markerCluster = new markerClusterer.MarkerClusterer({
    map: map,
    markers: markers,
    renderer: renderer,
    algorithm: algorithm });

Is there a way to handle multiple styles so that the icon and height and width change as it gets more zoomed in?

As you can see, I've tried converting one style and it works well, but the icons stay the same size relative to the screen the more you zoom in, until you get in close enough and it's just a mess of overlapping clusters. I can always downsize it if need be, but if the text size gets big enough, it overflows off the cluster, and I'd like to have something a little more elegant in mind anyway, closer to what the old version of the markerClusterer library had before.


Solution

  • You can have any logic in the renderer. Since you want to change the cluster icon based on the zoom level, you can simply add this bit of logic and change the marker icon accordingly.

    I would also suggest using SVG for your markers (some examples here: https://googlemaps.github.io/js-markerclusterer/public/renderers/) or HTML in combination with Advanced Markers.

    The below example uses Advanced Markers with HTML elements. Zoom in to see the cluster icon change.

    async function initMap() {
    
      // Request needed libraries.
      const { Map } = await google.maps.importLibrary("maps");
      const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
      const center = {
        lat: 0,
        lng: 0
      };
    
      const map = new Map(document.getElementById("map"), {
        center: center,
        zoom: 2,
        mapId: "4504f8b37365c3d0",
        zoomControl: true
      });
    
      const markers = [];
    
      // Define the max latitude on a mercator projection
      var maxLat = Math.atan(Math.sinh(Math.PI)) * 180 / Math.PI;
    
      // Loop and create many markers
      for (let i = 0; i < 2000; i++) {
    
        // Calculate a random lat and lng
        const lat = Math.floor(Math.random() * (maxLat * 2)) - maxLat;
        const lng = Math.floor(Math.random() * 360) - 180;
    
        const marker = new AdvancedMarkerElement({
          map: map,
          position: new google.maps.LatLng(lat, lng),
          title: "AdvancedMarkerElement"
        });
    
        markers.push(marker);
      }
    
      let algorithm = new markerClusterer.SuperClusterAlgorithm({
        maxZoom: 15
      });
    
      // Marker clusterer
      const cluster = new markerClusterer.MarkerClusterer({
        map: map,
        markers: markers,
        algorithm: algorithm,
        renderer: {
          render: ({
            count,
            position
          }, stats, map) => {
    
            // Create a custom cluster HTML element to be used with an AdvancedMarker
            const el = document.createElement("div");
    
            // Change appearance based on current zoom
            el.className = map.getZoom() > 2 ? 'cluster red' : 'cluster';
            
            // Set content
            el.textContent = String(count);
         
            // Return AdvancedMarkerElement
            return new AdvancedMarkerElement({
              position: position,
              content: el,
              title: "AdvancedMarkerElement Cluster"
            });
          }
        }
      });
    }
    
    initMap();
    #map {
      height: 180px;
    }
    
    .cluster {
      font-size: 1.5em;
      background-color: yellow;
      padding: .25em .5em;
      transform: rotate(5deg);
    }
    
    .red {
      color: white;
      font-size: 2em;
      font-weight: bold;
      background-color: red;
      transform: rotate(-5deg);
    }
    <div id="map"></div>
    <script src="https://unpkg.com/@googlemaps/markerclusterer/dist/index.min.js"></script>
    
    <!-- prettier-ignore -->
    <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: "AIzaSyCkUOdZ5y7hMm0yrcCQoCvLwzdM6M8s5qk", v: "beta"});</script>