rr-leaflet

How to show graticule coordinates along axes in leaflet


Is it possible to add graticule tick marks and coordinates around the edges of a leaflet plot? Something like the screenshot below but with lon/lat values included? I looked online but did not find a solution.

map <- leaflet() %>%
  addTiles() %>%
  setView(lng = -118.88, lat = 38.1341, zoom = 8) #|>
  #addCoordinates()
map

enter image description here


Solution

  • I combined the compass-solution with the autograticule plugin.

    1. Copy the code from autograticule
    2. Adjust as needed
    3. Add it to the leaflet map using htmltools out

    Adjustments

    1 - you can specify the distance of graticule lines depending on the zoom level. Just adjust the if-else setting the lineOffset in below code.

    2 - at the end of the code, I added some css to adjust the style of the labels, adjust the fontsize / color to your liking.

    3 - the graticule labels are shown within the leaflet map, therefore I had to make sure, that the existing UI does not obstruct the labels. So the Latitude labels are offset from the left edge by 4 %, the Longitude 5 % from the bottom. Adjust this as needed.

    Code

    
    library(leaflet)
    library(htmltools) 
    
    map <- leaflet() %>%
      addTiles() %>%
      setView(lng = -118.88, lat = 38.1341, zoom = 8) %>%
      addScaleBar(position = "bottomleft", options = scaleBarOptions(maxWidth = 100, metric = TRUE, imperial = FALSE)) %>%
      addControl(HTML('
    <div style="width: 90px;height: 90px;">
        <img src="https://clipart-library.com/images/rTnRjnAGc.png" style="width: 100%; width: 100%; opacity: 0.7;">
    </div>'), position = "topright", className = "leaflet-control-nobg") %>%
      htmlwidgets::onRender("
        function(el, x) {
          // Store reference to map
          var map = this;
          
          // Define the AutoGraticule plugin directly
          L.AutoGraticule = L.LayerGroup.extend({
            options: {
              redraw: 'move',
              minDistance: 20
            },
            
            initialize: function(options) {
              L.LayerGroup.prototype.initialize.call(this);
              L.Util.setOptions(this, options);
              this._layers = {};
              this._zoom = -1;
            },
            
            onAdd: function(map) {
              this._map = map;
              map.on('viewreset ' + this.options.redraw, this._reset, this);
              this._reset();
            },
            
            onRemove: function(map) {
              map.off('viewreset ' + this.options.redraw, this._reset, this);
              this.clearLayers();
            },
            
            _reset: function() {
              this.clearLayers();
              
              var currentZoom = this._map.getZoom(); // 0 completely zoomed out, 18-20 completely zoomed in
              var bounds = this._map.getBounds();
              var sw = bounds.getSouthWest();
              var ne = bounds.getNorthEast();
              
              // Calculate a better position for longitude labels (5% from bottom of map)
              var labelLat = sw.lat + (ne.lat - sw.lat) * 0.05;
              
              // Determine line offset of graticules based on zoom level using a formula
              //var lineOffset = Math.round(40 * Math.pow(0.6, currentZoom) * 100) / 100;
              // or manually
              var lineOffset;
              if (currentZoom <= 2) {
                lineOffset = 30; 
              } else if (currentZoom <= 4) {
                lineOffset = 10; 
              } else if (currentZoom <= 6) {
                lineOffset = 5;  
              } else if (currentZoom <= 8) {
                lineOffset = 2;  
              } else if (currentZoom <= 10) {
                lineOffset = 1; 
              } else if (currentZoom <= 12) {
                lineOffset = 0.5; 
              }else {
                lineOffset = 0.1; 
              }
              
              console.log('Current Zoom: ', currentZoom, ', LineOffset: ', lineOffset);
             
              
              // longitude lines
              for (var lng = Math.floor(sw.lng / lineOffset) * lineOffset; 
                   lng <= Math.ceil(ne.lng / lineOffset) * lineOffset; 
                   lng += lineOffset) {
                
                var roundedLng = Math.round(lng * 100) / 100; // Round to 2 decimal places
                var line = L.polyline([[sw.lat, roundedLng], [ne.lat, roundedLng]], 
                                      {color: '#888', weight: 1, opacity: 0.5});
                this.addLayer(line);
                
                if (Math.abs(roundedLng) > 0.001) { // Avoid adding label at exactly 0
                  // Format label based on line offset - show decimals only when needed
                  var lngLabel = roundedLng.toFixed(lineOffset < 1 ? 1 : 0) + '°';
                  
                  var lngMarker = L.marker([labelLat, roundedLng], {
                    icon: L.divIcon({
                      className: 'leaflet-graticule-label',
                      html: lngLabel,
                      iconSize: [0, 0],
                      iconAnchor: [0, 0]
                    })
                  });
                  this.addLayer(lngMarker);
                }
              }
              
              // Draw latitude lines
              for (var lat = Math.floor(sw.lat / lineOffset) * lineOffset; 
                   lat <= Math.ceil(ne.lat / lineOffset) * lineOffset; 
                   lat += lineOffset) {
                
                var roundedLat = Math.round(lat * 100) / 100; // Round to 2 decimal places
                var line = L.polyline([[roundedLat, sw.lng], [roundedLat, ne.lng]], 
                                      {color: '#888', weight: 1, opacity: 0.5});
                this.addLayer(line);
                
                if (Math.abs(roundedLat) > 0.001) { // Avoid adding label at exactly 0
                  // Format label based on line offset - show decimals only when needed
                  var latLabel = roundedLat.toFixed(lineOffset < 1 ? 1 : 0) + '°';
                                                                                    // adjust left offset
                  var latMarker = L.marker([roundedLat, sw.lng + (ne.lng - sw.lng) * 0.04], {
                    icon: L.divIcon({
                      className: 'leaflet-graticule-label',
                      html: latLabel,
                      iconSize: [0, 0],
                      iconAnchor: [0, 0]
                    })
                  });
                  this.addLayer(latMarker);
                }
              }
              
              // Add some CSS for the labels
              if (!document.getElementById('graticule-style')) {
                var style = document.createElement('style');
                style.id = 'graticule-style';
                style.innerHTML = '.leaflet-graticule-label { color: black; font-weight: bold; font-size: 14px; white-space: nowrap; text-shadow: 1px 1px 1px #fff, -1px 1px 1px #fff, 1px -1px 1px #fff, -1px -1px 1px #fff; }';
                document.head.appendChild(style);
              }
            }
          });
          
          L.autoGraticule = function(options) {
            return new L.AutoGraticule(options);
          };
          
          // Add the graticule to the map
          var graticule = L.autoGraticule().addTo(map);
        }
      ")
    
    
    
    
    map <- htmlwidgets::prependContent(map, 
                                       tags$style(".leaflet-control-nobg { background: none !important; box-shadow: none !important; border: none !important; }")
    )
    
    map