Skip to content

Instantly share code, notes, and snippets.

@jcheng5
Last active December 15, 2022 16:01
Show Gist options
  • Save jcheng5/c084a59717f18e947a17955007dc5f92 to your computer and use it in GitHub Desktop.
Save jcheng5/c084a59717f18e947a17955007dc5f92 to your computer and use it in GitHub Desktop.
Using arbitrary Leaflet plugins with Leaflet for R

Using arbitrary Leaflet JS plugins with Leaflet for R

The Leaflet JS mapping library has lots of plugins available. The Leaflet package for R provides direct support for some, but far from all, of these plugins, by providing R functions for invoking the plugins.

If you as an R user find yourself wanting to use a Leaflet plugin that isn't directly supported in the R package, you can use the technique shown here to load the plugin yourself and invoke it using JS code.

library(leaflet)
library(htmltools)
library(htmlwidgets)
# This tells htmlwidgets about our plugin name, version, and
# where to find the script. (There's also a stylesheet argument
# if the plugin comes with CSS files.)
esriPlugin <- htmlDependency("leaflet.esri", "1.0.3",
src = c(href = "https://cdn.jsdelivr.net/leaflet.esri/1.0.3/"),
script = "esri-leaflet.js"
)
# A function that takes a plugin htmlDependency object and adds
# it to the map. This ensures that however or whenever the map
# gets rendered, the plugin will be loaded into the browser.
registerPlugin <- function(map, plugin) {
map$dependencies <- c(map$dependencies, list(plugin))
map
}
leaflet() %>% setView(-122.23, 37.75, zoom = 10) %>%
# Register ESRI plugin on this map instance
registerPlugin(esriPlugin) %>%
# Add your custom JS logic here. The `this` keyword
# refers to the Leaflet (JS) map object.
onRender("function(el, x) {
L.esri.basemapLayer('Topographic').addTo(this);
}")
# This example shows the ability to pass extra R data to onRender.
# At the time of this writing it requires a custom build of htmlwidgets:
# devtools::install_github("ramnathv/htmlwidgets@joe/feature/onrender-data")
#
# Track the progress of this functionality at
# https://github.com/ramnathv/htmlwidgets/pull/202
library(leaflet)
library(htmltools)
library(htmlwidgets)
library(dplyr)
heatPlugin <- htmlDependency("Leaflet.heat", "99.99.99",
src = c(href = "http://leaflet.github.io/Leaflet.heat/dist/"),
script = "leaflet-heat.js"
)
registerPlugin <- function(map, plugin) {
map$dependencies <- c(map$dependencies, list(plugin))
map
}
leaflet() %>% addTiles() %>%
fitBounds(min(quakes$long), min(quakes$lat), max(quakes$long), max(quakes$lat)) %>%
registerPlugin(heatPlugin) %>%
onRender("function(el, x, data) {
data = HTMLWidgets.dataframeToD3(data);
data = data.map(function(val) { return [val.lat, val.long, val.mag*100]; });
L.heatLayer(data, {radius: 25}).addTo(this);
}", data = quakes %>% select(lat, long, mag))
@laurapoggio-sptools
Copy link

laurapoggio-sptools commented Apr 18, 2018

I am trying to use the SideBySide plugin, however without any luck at the moment. I tried the suggestion of downloading the file and this answer here. Below what I am trying to run (using chrome under fedora 27). I can see the first layer but not the handle to compare the two.

Any help would be greatly appreciated. Thanks

LeafletSideBySidePlugin <- htmlDependency("leaflet-side-by-side","2.0.0",
                                          src = normalizePath("/full/path/to/folder/with/js"),
                                          script="leaflet-side-by-side.js")

registerPlugin <- function(map, plugin) {
  map$dependencies <- c(map$dependencies, list(plugin))
  map
}

leaflet() %>%
 registerPlugin(LeafletSideBySidePlugin) %>%
  onRender("
           function(el, x) {
           var mylayer1 = L.tileLayer(
           'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
           maxZoom: 18
           }).addTo(this);
           var mylayer2 = L.tileLayer(
           '//stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.png',{
           maxZoom: 14
           }).addTo(this);
           L.control.sideBySide(mylayer1, mylayer2).addTo(this);
          } ")

@chintanp
Copy link

This gist is really helpful and on point and exactly what I was looking for. I tried using the method to use the "Leaflet playback" plugin.

My code looks like this:

# Draw a line from startPoint to Endpoint on the map
                    leafletProxy(mapId = "map") %>%
                        onRender(
                            "function(el, x, data) {
                                // Initialize playback
                                var playback = new L.Playback(this, data, onPlaybackTimeChange);
                                console.log('Inside leaflet playback');
                            };
                            
                            // A callback so timeline is set after changing playback time
                            function onPlaybackTimeChange (ms) {
                                //timeline.setCustomTime(new Date(ms));
                            };", data = geojson_ends
                        )

I register the plugin with the first call to leaflet where I create the map. Two functions are created inside the onRender as L.PLayback needs a callback function onPalybackTimeChange. The leafletproxy is just used for updating the plot. As such I don't think this is working. No error, just no change in behavior.

  • Is there a way to debug this? I tried putting a console.log to see it gets printed, no luck.
  • Is there a way I can use "Developer tools" to inspect the code in the the browser? If yes, which file should I look for? Apparently, shiny creates a bunch of files.
  • Any other hints/comments?

@Jigar-S
Copy link

Jigar-S commented Dec 31, 2018

This is really helpful information. We are trying implement the 'leaflet-measure' plugin but it simply does not render properly (renders without any icons being displayed). This post seems like a promising solution, but on trial and error, it fails to load any plugins that are of type leaflet-control. Dunno if this is the correct place to post, but I would really appreciate some help to rectify the issue.
Here's my code to use the leaflet-control type plugins using this method.

`library(leaflet)
library(htmltools)
library(htmlwidgets)

LeafletMeasure <- htmlDependency("leaflet-measure","3.1.0",
src = c(href="https://github.com/ljigis/leaflet-measure/src"),
script="leaflet-measure.js",
stylesheet = "https://github.com/ljigis/leaflet-measure/scss/leaflet-measure.scss")

registerPlugin <- function(map, plugin) {
map$dependencies <- c(map$dependencies, list(plugin))
map
}

leaflet() %>% addTiles() %>%
setView(lng = 12, lat = 50, zoom = 4) %>%
registerPlugin(LeafletMeasure) %>%
onRender("
function(el, x) {
measureControl.addTo(this);}")`

The plugin has a scss that is in a different folder from the standard src folder. I have tried referencing the file through the Github repository as well as local copy.

@jcheng5
Copy link
Author

jcheng5 commented Feb 28, 2019

Wow, sorry, I didn't know there were so many messages posted here over the years. @Jigar-S, leaflet-measure is already bundled with the leaflet R package, just do leaflet() %>% addMeasure() for example.

For the rest of you, I'd focus on the following for debugging:

  1. Are my JS/CSS resources being properly attached? (BTW, you can't use .scss or .less files from htmlDependency directly, they have to be compiled to .css first for the browser to understand them.) You can use Chrome Developer Tools to see if there are any errors in the Console tab about JS and CSS URLs not being downloaded, and look in the Elements tab to see if the proper JS/CSS links are in the <head> of the document.
  2. Am I getting any JS errors? Again, use the Chrome Developer Tools' Console tab to see if there are errors. If there are, you need to fix those first.
  3. If the previous steps pass, or you have trouble figuring out the cause of the errors, then try the JS Debugger. One easy way is to add the statement debugger; to the start of your onRender function's JS function. Then, if you load the page with Chrome Developer Tools enabled, the JS debugger should pause just inside your JS function. With the debugger paused, you can examine the values of this, el, x, etc. to see if everything matches your expectations.

If you have continued problems, feel free to file an issue on the rstudio/leaflet repository, or drop by https://community.rstudio.com/c/shiny.

@helgasoft
Copy link

helgasoft commented May 20, 2019

@jcheng5, thank you for this thread and all the directions.
Sharing our successful integration of one JS plugin for tile-layer opacity.
Shiny app code is here. The js/css files have to be downloaded before execution.
The bulk of the work was to find the Leaflet JS 'hook-up' objects, which are not documented.

@micahwilhelm
Copy link

@helgasoft, thank you for sharing your code! Do you know the 'hook-up' object syntaxes for polygons, polylines or markers?
My attempt to integrate the leaflet-groupedlayercontrol plugin have so far been unsuccessful. Thanks in advance for any tips you might offer.

@helgasoft
Copy link

@micahwilhelm, here is the code on how to integrate this plugin.
I wish someone could find a way to add group markers from R, not only from Javascript...

@michaelgaunt404
Copy link

michaelgaunt404 commented Sep 27, 2020

@helgasoft
@jcheng5

Great work! do you know how you would do something like this with a map that is already constructed with mapview?

I would like to make the resulting map's legend say:

Places
|-> names
|-> village
Other
|-> Changed Name

install.packages("mapview")
install.packages("dplyr")
install.packages("magrittr")
library(mapview)
library(dplyr)
library(magrittr)

names = breweries %>%
select(brewery)

village = breweries %>%
select(village)

Complicated Name_layer = breweries %>%
select(number.of.types)

mapview(names, legend = F) +
mapview(village, legend = F) +
mapview(Complicated Name_layer, legend = F)

Thank you so much!!

image

@nash1119
Copy link

@jcheng5 and community

Does anyone know how to successfully pass R objects into the JS logic?

My attempt:

library(leaflet)
library(htmltools)
library(htmlwidgets)
library(fontawesome)
library(jsonlite)

beautifyPlugin <- htmlDependency("Leaflet.BeautifyMarker", "1.0.9",
                                 src = c(href="https://cdn.jsdelivr.net/npm/beautifymarker@1.0.9/"),
                                 script = "leaflet-beautify-marker-icon.min.js",
                                 stylesheet = "leaflet-beautify-marker-icon.min.css"
)

fontawesomePlugin <- htmlDependency("Leaflet.BeautifyMarkerFA", "1.0.9",
                                    src = c(href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/"),
                                    stylesheet = "font-awesome.min.css"
)

registerPlugin <- function(map, plugin) {
  map$dependencies <- c(map$dependencies, list(plugin))
  map
}

iconName <- list(icon = 'leaf')
toJSON(iconName)
#> {"icon":["leaf"]}
  
leaflet() %>% setView(-122.23, 37.75, zoom = 10)%>%
  addTiles()%>%
  registerPlugin(fontawesomePlugin)%>%
  registerPlugin(beautifyPlugin) %>%
  onRender("function(el, x, iconName) {
          options = {
              icon: data.icon,
              iconShape: 'marker'
           };
           L.marker([37.77, -122.40] ,
           {icon: L.BeautifyIcon.icon(options)
           }).addTo(this).bindPopup('test');
           }")

Created on 2020-10-20 by the reprex package (v0.3.0)

@helgasoft
Copy link

@nash1119 - why not just paste the value inside the JS code string:

library(leaflet)
library(htmltools)
library(htmlwidgets)
beautifyPlugin <- htmlDependency( "Beautify", "1.0.9",
                                 src = c(href="https://cdn.jsdelivr.net/npm/beautifymarker@1.0.9/"),
                                 script = "leaflet-beautify-marker-icon.min.js",
                                 stylesheet = "leaflet-beautify-marker-icon.min.css"
)
fontawesomePlugin <- htmlDependency( "fontawesome", "4.5.0",
                                    src = c(href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/"),
                                    stylesheet = "font-awesome.min.css"
)
registerPlugin <- function(map, plugin) {
  map$dependencies <- c(map$dependencies, list(plugin))
  map
}
myIcon <- 'leaf'
leaflet() %>% setView(-122.23, 37.75, zoom = 10) %>% addTiles()%>%
 registerPlugin(fontawesomePlugin) %>% 
 registerPlugin(beautifyPlugin) %>%
 onRender(paste0("function(el, x) {
	L.marker([37.77, -122.40] , 
		{icon: L.BeautifyIcon.icon({ icon:'" ,myIcon, "', iconShape: 'marker' }) }).addTo(this).bindPopup('test');
 }"))

@nash1119
Copy link

@helgasoft thank you!

@nash1119
Copy link

nash1119 commented Nov 9, 2020

@rickyars @Bignoozer @chintanp or anyone, did you ever find a solution to why this does not seem to work with leafletProxy()? I am currently having this issue. Thanks!

@viniciuszendron
Copy link

@nash1119 @rickyars @Bignoozer @chintanp please take a look at the question below:

https://stackoverflow.com/questions/52846472/leaflet-plugin-and-leafletproxy-with-polylinedecorator-as-example

It worked for my case as a workaround. You need to anticipate what is coming by creating a function associated to an event listener inside onRender, and call this directly on leaflet, not leafletProxy.

Maybe @jcheng5 could have some idea on why the main approach described in this gist doesn't work well with leafletProxy.

@oggioniale
Copy link

Hello,
some of you have experience about integration of Leaflet.LayerTreePlugin with R Shiny

library(htmltools)
library(htmlwidgets)
library(dplyr)

layerTreePlugin <- htmltools::htmlDependency(
  "Leaflet.LayerTreePlugin", "1.0.0",
  src = "./www/js/",
  script = "leaflet-layer-tree-control.js"
)

registerPlugin <- function(map, plugin) {
  map$dependencies <- c(map$dependencies, list(plugin))
  map
}

leaflet() %>% addTiles() %>%
  registerPlugin(layerTreePlugin) %>%
  onRender("function(el, x) {
  function buildContentFromLayer(layer) {
		var content = '<table>';
		var properties = layer.feature.properties;
		for (var i in properties) {
			content += '<tr><td>' + i + '</td><td>' + properties[i] + '</td></tr>';
		}
		content += '</table>';
		return content;
	}
  
	// Icons
	var greenIcon = L.icon({
		iconUrl: 'https://leafletjs.com/examples/custom-icons/leaf-green.png',
		shadowUrl: 'https://leafletjs.com/examples/custom-icons/leaf-shadow.png',

		iconSize: [38, 95],
		shadowSize: [50, 64],
		iconAnchor: [22, 94],
		shadowAnchor: [4, 62],
		popupAnchor: [-3, -76]
	});
	
	var tree = [{code: 'root', name: 'All the Layers', active: true,
			selectedByDefault: false,
			openByDefault: true,
			childLayers: [
				{
					code: 'base',
					name: 'Base layers',
					active: true,
					selectedByDefault: false,
					openByDefault: true,
					childLayers: [
						{
							code: 'osm',
							name: 'OpenStreetMap',
							active: true,
							selectedByDefault: false,
							openByDefault: true,
							childLayers: [],
							selectType: 'MULTIPLE',
							serviceType: 'OSM',
							params: {
								url: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
							}
						},
						{
							code: 'google',
							name: 'Google',
							active: true,
							selectedByDefault: true,
							openByDefault: true,
							childLayers: [],
							selectType: 'NONE',
							serviceType: 'GOOGLE',
							params: {}
						},
						{
							code: 'google_terrain',
							name: 'Google Terrain',
							active: true,
							selectedByDefault: false,
							openByDefault: true,
							childLayers: [],
							selectType: 'NONE',
							serviceType: 'GOOGLE_TERRAIN',
							params: {}
						}
					],
					selectType: 'SINGLE',
					serviceType: null,
					params: {}
				},
				{
					code: 'overlays',
					name: 'Overlays',
					active: true,
					selectedByDefault: false,
					openByDefault: true,
					childLayers: [
						{
							code: '50m_coastline',
							name: 'rsg:50m_coastline',
							active: true,
							selectedByDefault: true,
							openByDefault: true,
							selectType: 'MULTIPLE',
							serviceType: 'WFS',
							coordinateSystem: 'EPSG:4326',
							onPopup: function (layer) {
								return buildContentFromLayer(layer);
							},
							params: {
								request: 'getFeature',
								service: 'WFS',
								typeName: 'rsg:50m_coastline',
								style: '{\"stroke\":true,\"fillColor\":\"violet\",\"border\":\"orange\",\"weight\":3,\"opacity\":0.5,\"color\":\"red\",\"dashArray\":\"5\",\"fillOpacity\":0.1}',
								  version: '1.1.0',
								  outputFormat: 'application/json',
								  url: 'https://rsg.pml.ac.uk/geoserver/wfs',
								  maxFeatures: '25'
								},
								childLayers: [
								  {
								    code: 'MPA_SCOTLAND',
								    name: 'rsg:MPA_SCOTLAND',
								    active: true,
								    selectedByDefault: true,
								    openByDefault: true,
								    selectType: 'MULTIPLE',
								    serviceType: 'WFS',
								    coordinateSystem: 'EPSG:4326',
								    onPopup: function (layer) {
								      return buildContentFromLayer(layer);
								    },
								    params: {
								      request: 'getFeature',
								      service: 'WFS',
								      typeName: 'rsg:MPA_SCOTLAND',
								      style: '{\"stroke\":true,\"fillColor\":\"yellow\",\"border\":\"gray\",\"weight\":3,\"opacity\":0.5,\"color\":\"gray\",\"dashArray\":\"5\",\"fillOpacity\":0.1}',
								      version: '1.1.1',
								      outputFormat: 'application/json',
								      url: 'https://rsg.pml.ac.uk/geoserver/wfs',
								      maxFeatures: '25'
								    }
								  },
								  {
								    code: 'MMO_Fish_Shellfish_Cages_A',
								    name: 'rsg:MMO_Fish_Shellfish_Cages_A',
								    active: true,
								    selectedByDefault: false,
								    openByDefault: true,
								    selectType: 'MULTIPLE',
								    serviceType: 'WFS',
								    coordinateSystem: 'EPSG:4326',
								    onPopup: function (layer) {
								      return buildContentFromLayer(layer);
								    },
								    params: {
								      request: 'getFeature',
								      service: 'WFS',
								      typeName: 'rsg:MMO_Fish_Shellfish_Cages_A',
								      version: '1.1.1',
								      outputFormat: 'application/json',
								      url: 'https://rsg.pml.ac.uk/geoserver/wfs',
								      maxFeatures: '25'
								    }
								  }
								  ]
								},
{
  code: 'tiger_roads',
  name: 'tiger:tiger_roads',
  active: true,
  selectedByDefault: false,
  openByDefault: true,
  selectType: 'MULTIPLE',
  serviceType: 'WFS',
  coordinateSystem: 'EPSG:4326',
  onPopup: function (layer) {
    return buildContentFromLayer(layer);
  },
  params: {
    request: 'getFeature',
    service: 'WFS',
    typeName: 'tiger:tiger_roads',
    style: '{\"stroke\":true,\"fillColor\":\"green\",\"border\":\"cyan\",\"weight\":3,\"opacity\":0.5,\"color\":\"red\",\"dashArray\":\"3\",\"fillOpacity\":0.1}',
    version: '1.1.1',
    outputFormat: 'application/json',
    url: 'https://rsg.pml.ac.uk/geoserver/wfs',
    maxFeatures: '25'
  },
  childLayers: [
    {
      code: 'poi',
      name: 'tiger:poi',
      active: true,
      selectedByDefault: true,
      openByDefault: true,
      childLayers: [],
      selectType: 'MULTIPLE',
      serviceType: 'WFS',
      coordinateSystem: 'EPSG:4326',
      onPopup: function (layer) {
        return buildContentFromLayer(layer);
      },
      params: {
        request: 'getFeature',
        service: 'WFS',
        typeName: 'tiger:poi',
        version: '1.1.1',
        outputFormat: 'application/json',
        url: 'https://rsg.pml.ac.uk/geoserver/wfs',
        maxFeatures: '25',
        icon: greenIcon
      },
    }
    ]
}
],
selectType: 'MULTIPLE',
serviceType: null,
params: {}
}
],
selectType: 'NONE',
serviceType: null,
params: {}
}
];

// Base Layers
var layerBuilders = {
  GOOGLE: function (layerSettings) {
    return L.tileLayer('http://{s}.google.com/vt/lyrs=m&x={x}&y={y}&z={z}', {
      maxZoom: 20,
      subdomains: ['mt0', 'mt1', 'mt2', 'mt3']
    });
  },
  GOOGLE_TERRAIN: function (layerSettings) {
    return L.tileLayer('http://{s}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', {
      maxZoom: 20,
      subdomains: ['mt0', 'mt1', 'mt2', 'mt3']
    });
  }
  
};

// The Tree Control
new L.Control.LayerTreeControl({
  layerTree: tree,
  openByDefault: true,
  layerBuilders: layerBuilders,
  featureBuilders: {
    WFS: {
      zoom: L.Control.LayerTreeControl.WFSZoomFeature
    }
  }
}).addTo(this);

  }")

The map is empty without any Layer Tree

@nicksinus
Copy link

nicksinus commented Jul 16, 2021

I'm trying to use Leaflet.LabelTextCollision, but it doesn't work. What did i miss?

library(tidyverse)
library(leaflet)
library(htmltools)
library(htmlwigets)




label <- data.frame(
lat = c(61.09049, 56.89039, 57.52678, 60.74516, 56.92379, 64.54302, 56.25897, 56.49648, 56.27996, 56.74812, 59.93873, 56.77972, 56.85867, 60.08370, 56.25897),
lon = c(43.17148, 32.65250,  38.31669, 42.04732, 32.74461, 40.53712, 32.08586, 31.64108, 31.66774, 33.51162, 30.31623, 31.25263, 35.92083, 30.27957, 32.08586),
name_label = c("label_1", "label_2", "label_1000", "label_10000", "label_70000", "label_8", "label_999999", "label_777", "label_888888", "label_888", "label_999", "label_7", "label_9", "label_777777777", "label_999999999")
)




col_Plugin <- htmlDependency(
  "Leaflet.LabelTextCollision", "1.4.0",
  src = "./Leaflet.LabelTextCollision-master/dist",
  script = "L.LabelTextCollision.js"
)


registerPlugin <- function(map, plugin) {
  map$dependencies <- c(map$dependencies, list(plugin))
  map
}  


leaflet() %>%
  
  setView(35, 55, zoom = 4) %>%
  
  addProviderTiles(provider =  "OpenStreetMap") %>%
  
  registerPlugin(col_Plugin) %>%

  addLabelOnlyMarkers(lng = label$lon,
                      lat = label$lat,
                      label = lapply(label$name_label, HTML),
                      labelOptions = labelOptions(noHide = T,
                                                  textOnly = F,
                                                  direction = "top")
  ) %>% 
  
  onRender("function(el, x) {

  L.LabelTextCollision().addTo(this);}"

  )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment