Related
I have a Shiny app with a leaflet showing ~9,000 points on a grid (each point representing a 100 m x 100 m square). The app is animated, so that each point changes colour over time. The first version of the app used addPolygons() with the setShapeStyle() function from here to allow the polygons to change colour over time, while accounting for area that each point accounts for. The polygon app was great but super slow, so I changed to addCircleMarkers instead, coupled with setCircleMarkerStyle() from the same GitHub page. This is way faster and works well, BUT I have 2 problems - 1) at low zooms, my points overlap, and 2) at high zooms, my points are separated by space.
Can anyway help me apply the same animation solutions offered by the change of style as here, but applied to addCircles() so that I can use a set radius, or a similar solution? Ideas for making the points square, so that I don't end up with empty spaces at high zooms are also welcome.
libraries and fake data:
library(plyr)
library(dplyr)
library(tidyr)
library(lubridate)
library(shiny)
library(leaflet)
library(viridisLite)
nodes <- structure(list(node = 1:9, Lon = c(-60.1760758677342, -60.1768617891598,
-60.1664512653477, -60.1672369749724, -60.1680228296767, -60.1688085769102,
-60.1695943435806, -60.170380129055, -60.1711659327049), Lat = c(43.316878317912,
43.317580709354, 43.309714197049,43.310416744826, 43.311119197611,
43.311821734546, 43.312524176034, 43.3132266121, 43.31392913277
)), row.names = c(NA, -9L), class = "data.frame")
data <- nodes %>%
crossing(Date = seq(as_date("2020-01-01"), as_date("2020-03-15"), "1 day")) %>%
mutate(Density = abs(rnorm(675, 10, 10)),
Exceed = ifelse(Density > 20, 1, 0),
Layer = paste(node, "Tile", sep = "_"))
FirstDay <- data %>%
filter(Date == min(Date))
Helper functions (based on the Github page referenced):
leafletjs <- tags$head(
tags$script(HTML('
window.LeafletWidget.methods.setStyle = function(category, layerId, style){
var map = this;
if (!layerId){
return;
} else if (!(typeof(layerId) === "object" && layerId.length)){
layerId = [layerId];
}
style = HTMLWidgets.dataframeToD3(style);
layerId.forEach(function(d,i){
var layer = map.layerManager.getLayer(category, d);
if (layer){
layer.setStyle(style[i]);
}
});
};
')))
setCircleMarkerStyle <- function(map, layerId
, radius = NULL
, stroke = NULL
, color = NULL
, weight = NULL
, opacity = NULL
, fill = NULL
, fillColor = NULL
, fillOpacity = NULL
, dashArray = NULL
, options = NULL
, data = getMapData(map)
){
options <- c(list(layerId = layerId),
options,
filterNULL(list(stroke = stroke, color = color,
weight = weight, opacity = opacity,
fill = fill, fillColor = fillColor,
fillOpacity = fillOpacity, dashArray = dashArray
)))
if (length(options) < 2) { # no style options set
return()
}
# evaluate all options
options <- evalFormula(options, data = data)
# make them the same length (by building a data.frame)
options <- do.call(data.frame, c(options, list(stringsAsFactors=FALSE)))
layerId <- options[[1]]
style <- options[-1] # drop layer column
#print(list(style=style))
leaflet::invokeMethod(map, data, "setStyle", "marker", layerId, style);
}
UI and server
ui <- fluidPage(
leafletjs,
sidebarLayout(
sidebarPanel(sliderInput("dateSel", "Date",
min = min(data$Date), max = max(data$Date),
value = min(data$Date), step = 1, timeFormat = "%d %b %y",
animate = animationOptions(interval = 100, loop = FALSE))),
mainPanel(leafletOutput("MapAnimate"))))
server <- function(input, output, session) {
filteredData <- reactive({
data %>% filter(Date == input$dateSel)
})
output$MapAnimate <- renderLeaflet({
range <- range(data$Density)
palette <- colorNumeric(palette = viridis(100), domain = range)
leaflet(FirstDay) %>%
addTiles() %>%
addCircleMarkers(lng = ~Lon, lat = ~Lat, layerId = ~Layer,
fillColor = "lightgray", fill = TRUE,
color = "white", stroke = TRUE,
fillOpacity = 1, opacity = 1, weight = 2) %>%
leaflet::addLegend(pal = palette, values = range, opacity = 0.9, position = "topleft")
})
observe({
df.in <- filteredData()
range <- range(data$Density)
palette <- colorNumeric(palette = viridis(100), domain = range)
leafletProxy("MapAnimate", data = df.in) %>%
setCircleMarkerStyle(layerId = ~Layer,
fillColor = ~palette(Density),
color = ~ifelse(Exceed == 1, "red", "white"))
})
}
shinyApp(ui = ui, server = server)
I have icons on a leaflet plot which have different colors and shapes based on some variables in my data frame. I want to include a legend on the plot that shows what each shape and color combination represents. How should I do it?
To demonstrate with dummy data:
library(leaflet)
lat1= 36+runif(n=5,min=-1,max=1)
lon1 =-115+runif(n=5,min=-1,max=1)
lat2= 35+runif(n=5,min=-0.5,max=0.5)
lon2 =-110+runif(n=5,min=-0.5,max=0.5)
lat3= 34+runif(n=5,min=-0.5,max=0.5)
lon3 =-112+runif(n=5,min=-0.5,max=0.5)
data_all=rbind(data.frame(Longitude=lon1,Latitude=lat1,Group=sample(c(16,17,18),5,replace = TRUE),condition=sample(c("red","blue","green"),5,replace = TRUE),stringsAsFactors = FALSE),
data.frame(Longitude=lon2,Latitude=lat1,Group=sample(c(16,17,18),5,replace = TRUE),condition=sample(c("red","blue","green"),5,replace = TRUE),stringsAsFactors = FALSE),
data.frame(Longitude=lon3,Latitude=lat1,Group=sample(c(16,17,18),5,replace = TRUE),condition=sample(c("red","blue","green"),5,replace = TRUE),stringsAsFactors = FALSE))
# A function to create png images for each shape and color
pchIcons = function(pch = 1, width = 30, height = 30, bg = "transparent", col = NULL, ...) {
n = length(pch)
files = character(n)
# create a sequence of png images
for (i in seq_len(n)) {
f = tempfile(fileext = '.png')
png(f, width = width, height = height, bg = bg)
par(mar = c(0, 0, 0, 0))
plot.new()
points(.5, .5, pch = pch[i], col = col[i], cex = min(width, height) / 8, ...)
dev.off()
files[i] = f
}
files
}
### ---------
leaflet(data_all)%>% addTiles() %>%
addMarkers(
data = data_all,
icon = ~ icons(
iconUrl = pchIcons(pch= Group,width=40,height=40,col=condition,lwd=4),
popupAnchorX = 20, popupAnchorY = 0
))
Based on this post, using base64enc and creating fixed filenames instead of using tempfile:
# A function to create file names
filename <- function(pch,col) paste0(pch, '_', col, '.png')
# A function to create png images for each shape and color
pchIcons = function(pch = 1, width = 30, height = 30, bg = "transparent", col = NULL, ...) {
n = length(pch)
files = character(n)
# create a sequence of png images
for (i in seq_len(n)) {
f = filename(pch[i], col[i])
png(f, width = width, height = height, bg = bg)
par(mar = c(0, 0, 0, 0))
plot.new()
points(.5, .5, pch = pch[i], col = col[i], cex = min(width, height) / 8, ...)
dev.off()
files[i] = f
}
files
}
# A function to build the legend
build_legend <- function(){
paste(sapply(strsplit(unique(paste(data_all$Group,data_all$condition)), " "),
function(x){
paste0("<img src='data:image/png;base64,",
base64enc::base64encode(filename(x[[1]], x[[2]])),
"' width='16'; height='16'> ",
"Group=",x[[1]], " Condition=", x[[2]],
"<br/>" )}), collapse = " ")
}
# The plot
leaflet(data_all)%>% addTiles() %>%
addMarkers(
data = data_all,
icon = ~ icons(
iconUrl = pchIcons(pch= Group,width=40,height=40,col=condition,lwd=4),
popupAnchorX = 20, popupAnchorY = 0)) %>%
addControl(html = build_legend(), position = "bottomleft")
I have different types of observations and I want to display them on leaflet using different shapes and colors. Is it possible to use diamond, triangle, star and other shapes in leaflet in R?
I have providing dummy data and created circle markers with differnt colors.
library(leaflet)
lat1= 36+runif(n=5,min=-1,max=1)
lon1 =-115+runif(n=5,min=-1,max=1)
lat2= 35+runif(n=5,min=-0.5,max=0.5)
lon2 =-110+runif(n=5,min=-0.5,max=0.5)
lat3= 34+runif(n=5,min=-0.5,max=0.5)
lon3 =-112+runif(n=5,min=-0.5,max=0.5)
data_all=rbind(data.frame(Longitude=lon1,Latitude=lat1,Group=1),
data.frame(Longitude=lon2,Latitude=lat2,Group=2),
data.frame(Longitude=lon3,Latitude=lat3,Group=3))
pal <- colorFactor(c("red","blue","purple"), domain = c(1,2,3))
leaflet(data_all) %>% addTiles() %>%
addCircleMarkers(~Longitude, ~Latitude,popup=~paste0("Group= ",data_all$Group),
radius = 10,
color = ~pal(Group),
stroke = FALSE, fillOpacity = 1
)
Here's a solution using base R plotting symbols. Basically, you create a series of temporary png files with the desired shape which you then use to represent your data according to the group.
library(leaflet)
# this is modified from
# https://github.com/rstudio/leaflet/blob/master/inst/examples/icons.R#L24
pchIcons = function(pch = 1, width = 30, height = 30, bg = "transparent", col = "black", ...) {
n = length(pch)
files = character(n)
# create a sequence of png images
for (i in seq_len(n)) {
f = tempfile(fileext = '.png')
png(f, width = width, height = height, bg = bg)
par(mar = c(0, 0, 0, 0))
plot.new()
points(.5, .5, pch = pch[i], col = col[i], cex = min(width, height) / 8, ...)
dev.off()
files[i] = f
}
files
}
lat1= 36+runif(n=5,min=-1,max=1)
lon1 =-115+runif(n=5,min=-1,max=1)
lat2= 35+runif(n=5,min=-0.5,max=0.5)
lon2 =-110+runif(n=5,min=-0.5,max=0.5)
lat3= 34+runif(n=5,min=-0.5,max=0.5)
lon3 =-112+runif(n=5,min=-0.5,max=0.5)
data_all=rbind(data.frame(Longitude=lon1,Latitude=lat1,Group=1),
data.frame(Longitude=lon2,Latitude=lat2,Group=2),
data.frame(Longitude=lon3,Latitude=lat3,Group=3))
shapes = c(5, 6, 8) # base R plotting symbols (http://www.statmethods.net/advgraphs/parameters.html)
iconFiles = pchIcons(shapes, 40, 40, col = c("red", "blue", "purple"), lwd = 4)
leaflet(data_all) %>% addTiles() %>%
addMarkers(
data = data_all,
icon = ~ icons(
iconUrl = iconFiles[Group],
popupAnchorX = 20, popupAnchorY = 0
),
popup=~paste0("Group= ",data_all$Group)
)
Obviously, you could use other png files in addMarkers.
This solution is based on a rather hidden example from https://github.com/rstudio/leaflet/blob/master/inst/examples/icons.R#L24
My leaflet map looks something like this:
library(sp)
library(leaflet)
circleFun <- function(center = c(0,0),diameter = 1, npoints = 100){
r = diameter / 2
tt <- seq(0,2*pi,length.out = npoints)
xx <- center[1] + r * cos(tt)
yy <- center[2] + r * sin(tt)
Sr1 = Polygon(cbind(xx, yy))
Srs1 = Polygons(list(Sr1), "s1")
SpP = SpatialPolygons(list(Srs1), 1:1)
return(SpP)
}
Circle.Town <- circleFun(c(1,-1),2.3,npoints = 100)
df1 <- data.frame(long=c(0.6,1,1.4), lat=c(-2, -.8, -0.2), other=c('a', 'b', 'c'), VAM=c(10,8,6),
type=c('Public', 'Public', 'Private'), id=c(1:3)) %>%
mutate(X=paste0('<strong>id: </strong>',
id,
'<br><strong>type</strong>: ',
type,
'<br><strong>VAM</strong>: ',
VAM))
# Create a continuous palette function
pal <- colorNumeric(
palette = "RdYlBu",
domain = df1$VAM
)
leaflet(height = "400px") %>%
addTiles() %>%
addPolygons(data = Circle.Town, color = 'green', fillOpacity = .7) %>%
addCircleMarkers(data = df1, lat = ~lat, lng =~long,
radius = ~VAM, popup = ~as.character(X),
fillColor = ~pal(VAM),
stroke = FALSE, fillOpacity = 0.8,
clusterOptions = markerClusterOptions()) %>%
addLegend(position = "topright",
pal = pal, values = df1$VAM,
title = "VAM",
opacity = 1
) %>%
setView(lng = 1, lat = -1, zoom = 8)
Right now, I get a popup when I click one of the circles. Is it possible to get the information when I hover the mouse instead of click? Ideally, I would like something like this.
Thanks!
This may have been added to the leaflet package since this question was posed a year ago, but this can be done via the label argument. I am using leaflet R package version 1.1.0.
Read the data in as above:
library(sp)
library(leaflet)
library(dplyr)
circleFun <- function(center = c(0,0),diameter = 1, npoints = 100){
r = diameter / 2
tt <- seq(0,2*pi,length.out = npoints)
xx <- center[1] + r * cos(tt)
yy <- center[2] + r * sin(tt)
Sr1 = Polygon(cbind(xx, yy))
Srs1 = Polygons(list(Sr1), "s1")
SpP = SpatialPolygons(list(Srs1), 1:1)
return(SpP)
}
Circle.Town <- circleFun(c(1,-1),2.3,npoints = 100)
df1 <- data.frame(long=c(0.6,1,1.4), lat=c(-2, -.8, -0.2), other=c('a', 'b', 'c'), VAM=c(10,8,6),
type=c('Public', 'Public', 'Private'), id=c(1:3)) %>%
mutate(X=paste0('<strong>id: </strong>',
id,
'<br><strong>type</strong>: ',
type,
'<br><strong>VAM</strong>: ',
VAM))
# Create a continuous palette function
pal <- colorNumeric(
palette = "RdYlBu",
domain = df1$VAM
)
But create a list of labels instead of vector:
labs <- as.list(df1$X)
And then lapply the HTML function over that list within the label argument. Note to use label instead of popup.
library(htmltools)
leaflet(height = "400px") %>%
addTiles() %>%
addPolygons(data = Circle.Town, color = 'green', fillOpacity = .7) %>%
addCircleMarkers(data = df1, lat = ~lat, lng =~long,
radius = ~VAM, label = lapply(labs, HTML),
fillColor = ~pal(VAM),
stroke = FALSE, fillOpacity = 0.8,
clusterOptions = markerClusterOptions()) %>%
addLegend(position = "topright",
pal = pal, values = df1$VAM,
title = "VAM",
opacity = 1
) %>%
setView(lng = 1, lat = -1, zoom = 8)
This method is described in an an answer to this SO question: R and Leaflet: How to arrange label text across multiple lines
There is more info on HTML in labels in leaflet documentation:
https://rstudio.github.io/leaflet/popups.html
Here is an alternative:
library(leaflet)
library(htmltools)
library(htmlwidgets)
yourmap <- leaflet(height = "400px") %>%
addTiles() %>%
addPolygons(data = Circle.Town, color = 'green', fillOpacity = .7) %>%
addCircleMarkers(data = df1, lat = ~lat, lng =~long,
radius = ~VAM, popup = ~as.character(X),
fillColor = ~pal(VAM),
stroke = FALSE, fillOpacity = 0.8,
clusterOptions = markerClusterOptions()) %>%
addLegend(position = "topright",
pal = pal, values = df1$VAM,
title = "VAM",
opacity = 1
) %>%
setView(lng = 1, lat = -1, zoom = 8)
setwd("~/Desktop/")
saveWidget(yourmap, file="yourmap.html")
In your desktop, you will have an html and a folder saved under yourmap. Open the leaflet.js file located in /pathTo/yourmap_files/leaflet-binding-1.0.1.9002.
In leaflet.js, scroll down to var popup = df.get(i, 'popup');
and paste just below:
marker.on('mouseover', function (e) {
this.openPopup();
});
marker.on('mouseout', function (e) {
this.closePopup();
});
Save and reopen yourmap.html file. Hover on one of your point!!
When I try to add a legend to a leaflet map for a leaflet map (using the Leaflet for R package) incorporated into a Shiny app, the legend does not show the colors of the color palette. Instead it only shows the colors specified for the NA values, in this case, white.
The app does the following:
First, it filters a set of data based on user inputs
Then it generates a choropleth map from the filtered data
This is the code I used to make the legend:
addLegend(position = "bottomleft",
pal = pal, values = shp.data()$stat.selected,
title = "Legend",
opacity = .5)
Where pal is a quantile color palette as follows
pal <-colorQuantile(c("#B2FF66","#66CC00","#4C9900","#336600","#193300"),
NULL, n = 5, na.color="#FFFFFF")
shp.data() is a reactive expression that is a shapefile filtered based on user inputs and stat_selected is the specific statistic that the user selects for mapping onto colors.
I get the following warnings:
Warning in is.na(x) :
is.na() applied to non-(list or vector) of type 'NULL'
Warning in is.na(values) :
is.na() applied to non-(list or vector) of type 'NULL'
I initially tried to make the legend following the example on the leaflet for R page and used the argument values = ~stat.selected for the addLegend function, but I got this error:
Error in UseMethod("doResolveFormula") :
no applicable method for 'doResolveFormula' applied to an object of class "NULL"
Earlier I had just a simple snippet that showed how to add legends. I did not use the ~ before the legend values as is the norm. I did the traditional dataframe$column and it works nicely.
This is now updated to see how it all fits together. Here is a full-fledged mapping run after creating all of the variable cuts, etc. The final cleansed data frame was called zipData
# create a full popup
# add some HTML for editing the styles
zipData$popUp <- paste('<strong>',zipData$Street, '</strong><br>',
'TIV = $',prettyNum(zipData$tiv, big.mark = ',',preserve.width = 'none'), '<br>',
'City: ', zipData$city, '<br>',
'YrBuilt = ', zipData$YearBuilt, '<br>',
'Construction = ', zipData$ConstructionCode, '<br>',
'Occupancy = ', zipData$OccupancyCode, '<br>',
'Premium = $' , prettyNum(zipData$Premium, big.mark = ',',preserve.width = 'none') , '<br>',
'GrossArea = ', prettyNum(zipData$GrossArea, big.mark = ',', preserve.width = 'none'), '<br>',
'RoofYr = ', zipData$RoofYearBuilt, '<br>')
# set color scale for key factor
colorsConst <- colorFactor(rainbow(4), zipData$ConstructionCode)
# color scales for numerical bins
colorstivValue <- colorFactor(palette = 'Accent', zipData$tivValueLvl)
colorsYrBuilt <- colorFactor(palette = 'Spectral', zipData$yrBuiltLvl)
colorsRoofYrBuilt <- colorFactor(palette = "YlOrRd", zipData$roofYrBuiltLvl)
# begin the leaflet map construction
# create the map opbject
m <- leaflet() %>%
addTiles() %>%
# add different tiles for different color schemes
addProviderTiles(providers$OpenStreetMap, group = 'Open SM') %>%
addProviderTiles(providers$Stamen.Toner, group = 'Toner') %>%
addProviderTiles(providers$CartoDB.Positron, group = 'CartoDB') %>%
addProviderTiles(providers$Esri.NatGeoWorldMap, group = 'NG World') %>%
setView(lng = -90, lat = 30, zoom = 10) %>%
##############################
# this section is for plotting the variables
# each variable below is a layer in the map
# construction
addCircleMarkers(data = zipData, lat = ~Lat, lng = ~Lon,
color = ~colorsConst(ConstructionCode), popup = zipData$popUp,
radius = 5, group = 'Construction') %>%
# tiv
addCircleMarkers(data = zipData, lat = ~Lat, lng = ~Lon,
color = ~colorstivValue(tivLvl), popup = zipData$popUp,
radius = ~tiv/20000, group = 'Bldg Value') %>%
# year built
addCircleMarkers(data = zipData, lat = ~Lat, lng = ~Lon,
color = ~colorsYrBuilt(yrBuiltLvl), popup = zipData$popUp,
radius = ~YearBuilt/250, group = 'Yr Built') %>%
######################################
# layer control
addLayersControl(
baseGroups = c('Open SM', 'Toner', 'Carto DB', 'NG World'),
overlayGroups = c('Construction',
'TIV',
'Yr Built'
),
options = layersControlOptions(collapsed = F)
) %>%
#################################################
add the legends for each of the variables
# construction
addLegend('bottomright', pal = colorsConst, values = zipData$ConstructionCode,
title = 'Construction Code',
opacity = 1) %>%
# tiv
addLegend('bottomleft', pal = colorstivValue, values = zipData$tivLvl,
title = 'TIV',
opacity = 1) %>%
# year built
addLegend('topleft', pal = colorsYrBuilt, values = zipData$yrBuiltLvl,
title = 'Yr Built',
opacity = 1)
m # Print the map
A portion of the map is shown below.
I was able to make the colors showing up by changing the way I was referencing the values column in the arguments of the AddLegend function. I put the stat.selected variable in double brackets, which seemed to fix the problem:
addLegend(position = "bottomleft",
pal = pal, values = shp.data()[[stat.selected]],
title = "Legend",
opacity = 1
)
For clarification, the stat.selected variable comes from the following switch statement:
stat.selected <- isolate(switch(input$var.stat,
"Total employment" = "tot_emp",
"Mean annual wage" = "a_mean",
"Mean hourly wage" = "h_mean",
"Location quotient" = "loc_quotient"
)
where "tot_emp", "a_mean", "h_mean", and "loc_quotient" are column names in the shp.data spatial polygons data frame.
I guess the problem was that I was trying to pass in the column name by variable using a $.
I'm still a fairly novice R user, so if anyone can explain why the example in the Leaflet for R documentation does not work in this case I would appreciate it.
I had the same message
Error in UseMethod("doResolveFormula") : no applicable method for 'doResolveFormula' applied to an object of class "NULL"
with
data <- data.frame(lng1 = c(1, 2, 3),
lng2 = c(2, 3, 4),
lat1 = c(1, 2, 3),
lat2 = c(2, 3, 4),
values = c(1, 2, 3))
pal_grid <- colorNumeric(palette = "YlGn", domain = data$values)
leaflet() %>%
addRectangles(lng1 = data$lng1, lat1 = data$lat1,
lng2 = data$lng2, lat2 = data$lat2,
fillColor = ~pal_grid(data$values),
fillOpacity = 0.2,
weight = 2, opacity = 0.5)
The solution is to provide to leaflet the data that you are using to create the element in the main call to leaflet() or in the call to any element that you add after that.
In the main call to leaflet():
data <- data.frame(lng1 = c(1, 2, 3),
lng2 = c(2, 3, 4),
lat1 = c(1, 2, 3),
lat2 = c(2, 3, 4),
values = c(1, 2, 3))
pal_grid <- colorNumeric(palette = "YlGn", domain = data$values)
leaflet(data = data) %>%
addRectangles(lng1 = data$lng1, lat1 = data$lat1,
lng2 = data$lng2, lat2 = data$lat2,
fillColor = ~pal_grid(data$values),
fillOpacity = 0.2,
weight = 2, opacity = 0.5)
At the moment of add elements:
data <- data.frame(lng1 = c(1, 2, 3),
lng2 = c(2, 3, 4),
lat1 = c(1, 2, 3),
lat2 = c(2, 3, 4),
values = c(1, 2, 3))
pal_grid <- colorNumeric(palette = "YlGn", domain = data$values)
leaflet() %>%
addRectangles(data = data,
lng1 = data$lng1, lat1 = data$lat1,
lng2 = data$lng2, lat2 = data$lat2,
fillColor = ~pal_grid(data$values),
fillOpacity = 0.2,
weight = 2, opacity = 0.5)`