What I like to do
I like to plot isochrones from multiple locations on a map so I can visually find the travel time from an arbitrary town to the closest location. It should look like a kernel density 2D plot:
library(purrr)
library(ggmap)
locations <- tibble::tribble(
~city, ~lon, ~lat,
"Hamburg", 9.992246, 53.550354,
"Berlin", 13.408163, 52.518527,
"Rostock", 12.140776, 54.088581
)
data <- map2_dfr(locations$lon, locations$lat, ~ data.frame(lon = rnorm(10000, .x, 0.8),
lat = rnorm(10000, .y, 0.7)))
ger <- c(left = min(locations$lon) - 1, bottom = min(locations$lat) - 1,
right = max(locations$lon) + 1, top = max(locations$lat) + 1)
get_stamenmap(ger, zoom = 7, maptype = "toner-lite") %>%
ggmap() +
stat_density_2d(data = data, aes(x= lon, y = lat, fill = ..level.., alpha = ..level..),
geom = "polygon") +
scale_fill_distiller(palette = "Blues", direction = 1, guide = FALSE) +
scale_alpha_continuous(range = c(0.1,0.3), guide = FALSE)
What I tried
You can easily get isochrones via osrm and plot them with leaflet. However, these isochrones are independent from each other. When I plot them they overlap each other.
library(osrm)
library(leaflet)
library(purrr)
library(ggmap)
locations <- tibble::tribble(
~city, ~lon, ~lat,
"Hamburg", 9.992246, 53.550354,
"Berlin", 13.408163, 52.518527,
"Rostock", 12.140776, 54.088581
)
isochrone <- map2(locations$lon, locations$lat,
~ osrmIsochrone(loc = c(.x, .y),
breaks = seq(0, 120, 30))) %>%
do.call(what = rbind)
isochrone#data$drive_times <- factor(paste(isochrone#data$min, "bis",
isochrone#data$max, "Minuten"))
factpal <- colorFactor("Blues", isochrone#data$drive_times, reverse = TRUE)
leaflet() %>%
setView(mean(locations$lon), mean(locations$lat), zoom = 7) %>%
addProviderTiles("Stamen.TonerLite") %>%
addPolygons(fill = TRUE, stroke = TRUE, color = "black",
fillColor = ~factpal(isochrone#data$drive_times),
weight = 0.5, fillOpacity = 0.6,
data = isochrone, popup = isochrone#data$drive_times,
group = "Drive Time") %>%
addLegend("bottomright", pal = factpal, values = isochrone#data$drive_time,
title = "Fahrtzeit")
How can I merge these isochrone so that they don't overlap?
Really cool question. What you want to do is merge the shapes by ID, so all the 0-30 minute areas are one shape, all the 30-60 minute areas are another, and so on. There are ways to do this with other spatial packages, but it seems well-suited to sf, which uses dplyr-style functions.
After you create isochrone, you can convert it to a sf object, make the same type of distance label, group by ID, and call summarise. The default when you summarize sf objects is just a spatial union, so you don't need to supply a function there.
library(sf)
library(dplyr)
iso_sf <- st_as_sf(isochrone)
iso_union <- iso_sf %>%
mutate(label = paste(min, max, sep = "-")) %>%
group_by(id, label) %>%
summarise()
I didn't have leaflet handy, so here's just the default print method:
plot(iso_union["label"], pal = RColorBrewer::brewer.pal(4, "Blues"))
I'm not sure what's up with the areas that have abrupt vertical edges, but those are in your plot as well.
I had a hard time using the map2 method you used because it does both a union as well as, I think, another set theory like function to create specific intervals. Instead, I would recommend creating a raster layer of the layers you create and apply one opacity to that one raster, like the ggmap example does. There's an excellent blog post that I stole alot of code from here (along with from user:camille).
It uses a different API that requires mapbox but it is free. Another limitation is that it won't return isocrones that are the size you like but I recreated it in another location where three points are closer together to prove the method.
I also didn't bother vectorizing the process of creating the isocrone web request so I leave that to someone smarter.
# First be sure to get your mapbox token
library(fasterize)
library(sf)
library(mapboxapi)
library(leaflet)
#mapboxapi::mb_access_token("Go get the token and put it here",
# install = TRUE, overwrite = TRUE)
isos1 <- mb_isochrone(
location = c("-149.883234, 61.185765"),
profile = "driving",
time = c(5,10,15),
)
isos2 <- mb_isochrone(
location = c("-149.928200, 61.191227"),
profile = "driving",
time = c(5,10,15),
)
isos3 <- mb_isochrone(
location = c("-149.939484, 61.160192"),
profile = "driving",
time = c(5,10,15),
)
library(sf)
library(dplyr)
isocrones <- rbind(isos1,isos2,isos3)
iso_sf <- st_as_sf(isocrones)
iso_union <- iso_sf %>%
group_by(time) %>%
summarise()
isos_proj <- st_transform(iso_sf, 32615)
template <- raster(isos_proj, resolution = 100)
iso_surface <- fasterize(isos_proj, template, field = "time", fun = "min")
pal <- colorNumeric("viridis", isos_proj$time, na.color = "transparent")
leaflet() %>%
addTiles() %>%
addRasterImage(iso_surface, colors = pal, opacity = 0.5) %>%
addLegend(values = isos_proj$time, pal = pal,
title = "Minutes of Travel") %>%
addMarkers(lat = c(61.185765, 61.191227, 61.160192), lng = c(-149.883234, -149.928200, -149.939484))
Related
I've got some GPS data (latlong) and I want to plot the GPS points and their connecting lines and color both by the time difference between the two GPS points. I've figured out how to color the points and convert the points to a LINESTRING but I can't figure out how to recolor the scale of the line.
I saw this post:
Color portions of sf LINESTRING by variable that shows how to break the linestrings into segments and color the segments by a categorical variable but as I have close to 100,000 observations I'd like to avoid just splitting my plot up into 99,999 pieces and also, my data is continuous.
Here's some toy data:
# Create some data points
fake_data = data.frame(Time = 1:6,
Long = c(-90.46200, -90.46160, -90.46170, -90.46150, -90.46100, -90.46240),
Lat = c(33.88540, 33.88750, 33.88520, 33.88340, 33.88540, 33.88150))
# Define as points
points = st_as_sf(fake_data, coords = c("Long", "Lat"), crs = 4326, remove = FALSE)
# Connect the dots
lines = points %>% summarize(do_union = FALSE) %>% st_cast("LINESTRING")
library(ggplot2)
# Plot
ggplot(data = points)+
geom_sf(aes(color = as.numeric(points$Time)))+
geom_sf(data = lines)+#, aes(color = numeric(points$Time[1:(length(points$Time)-1)])))+ #did not work
ylim(c(33.87, 33.89))+
xlim(c(-90.47, -90.45))+
scale_color_gradient(name = "Time", position="bottom" , low = "blue", high = "red")
Thank you!
I'm confident there are prettier ways to do this, but this works!
I needed to add in a group variable to use to generate linegroups. This was inspired by: https://stackoverflow.com/a/48979401/3642716 and their answer with how to solve for troops in the tidyverse dataset.
library(sf)
library(dplyr)
library(ggplot2)
# Create some data points
fake_data = data.frame(Time = 1:6,
Long = c(-90.46200, -90.46160, -90.46170, -90.46150, -90.46100, -90.46240),
Lat = c(33.88540, 33.88750, 33.88520, 33.88340, 33.88540, 33.88150),
group = 1)
# Define as points
points = st_as_sf(fake_data, coords = c("Long", "Lat"), crs = 4326, remove = FALSE)
# Connect the dots
lines <- fake_data
lines %<>% group_by(group) %>%
slice(rep(1:n(), each = 2)) %>%
slice(-c(1, n())) %>%
mutate(linegroup = lapply(1:(n()/2), function(x) rep(x, 2)) %>% unlist) %>%
ungroup %>%
group_by(linegroup) %>%
st_as_sf(coords = c("Long","Lat"), crs = 4326, remove = F) %>%
summarize( do_union = F) %>%
st_cast("LINESTRING")
# Plot
ggplot(data = points)+
geom_sf(aes(color = `Time`))+
geom_sf(data = lines, aes(color = `linegroup`))+#, aes(color = numeric(points$Time[1:(length(points$Time)-1)])))+ #did not work
ylim(c(33.881, 33.888))+
xlim(c(-90.463, -90.460))+
scale_color_gradient(name = "Time", position="bottom" , low = "blue", high = "red")
Looks like this:
I am trying to make a choropleth map as an html widget using the leaflet package. I don't want to deal with shiny for this. I have covid death time series data for each state. I would like to be able to click on states and have the corresponding time series graph to popup. I have gotten so close but my problem is that the graphs that popup when you click on a state do not correctly correspond to that state. For example, if you click on Ohio a West virginia map pops up.
Shapefile data: https://www.census.gov/cgi-bin/geo/shapefiles/index.php?year=2019&layergroup=States+%28and+equivalent%29
Covid data: https://data.cdc.gov/Case-Surveillance/United-States-COVID-19-Cases-and-Deaths-by-State-o/9mfq-cb36
library(tidyverse)
library(lubridate)
library(readr)
library(leaflet)
library(tigris)
library(rgdal)
library(leafpop)
states <- readOGR(dsn = "tl_2019_us_state", layer = "tl_2019_us_state")
covid_deaths<- read_csv("covid_deaths_usafacts.csv")
Clean_Deaths<- covid_deaths%>%
select(submission_date, state, tot_cases,new_case,tot_death,new_death)%>%
filter(new_death>=0)%>%
mutate(submission_date=as.Date(Clean_Deaths$submission_date, "%m/%d/%Y"))
my_list <- list()
loop<-for (i in unique(Clean_Deaths$state)) {
state<-Clean_Deaths%>% filter(state==i)
plot<-ggplot(state, aes(x = submission_date, y = new_death)) +
geom_line()+scale_x_date(date_breaks = "1 month",date_labels = "%b")+labs(title = i)
my_list[[i]] <- plot
}
m1 <- leaflet() %>%
addTiles() %>%
setView(lng = -120.5, lat = 44, zoom = 6)%>%
addPolygons(data = states,
fillColor = "red",
fillOpacity = 0.6,
color = "darkgrey",
weight = 1.5,
popup = popupGraph(my_list)
)
m1
I think you have abbreviations for state in Clean_Deaths$state (e.g., "NY") and you have full state names in states$NAME (e.g., "New York").
In your filter, you can convert from one to other. Your for loop can go through states$NAME which will match your data used in your map:
for (i in states$NAME) {
state<-Clean_Deaths%>% filter(state==state.abb[match(i, state.name)])
plot<-ggplot(state, aes(x = submission_date, y = new_death)) +
geom_line()+scale_x_date(date_breaks = "1 month",date_labels = "%b")+labs(title = i)
my_list[[i]] <- plot
}
Here is something comparable using lapply and simplified:
my_list <- lapply(states$NAME, function(i) {
Clean_Deaths %>%
filter(state == state.abb[match(i, state.name)]) %>%
ggplot(aes(x = submission_date, y = new_death)) +
geom_line() +
scale_x_date(date_breaks = "1 month",date_labels = "%b") +
labs(title = i)
})
As an aside, your mutate before this does not need the data frame referenced in the pipe:
mutate(submission_date=as.Date(submission_date, "%m/%d/%Y"))
Let me know if this addresses your problem.
I am able to produce a cartogram using cartogram::cartogram_cont() But then have difficulty in customising the styling.
I have used broom::tidy() and dplyr::left_join() to fortify the cartogram, but I think perhaps the tidy stage has interfered with the plotOrder. If possible, I will include the output cartograms.
I'm attempting to replicate this type of output, but within my locality. Plesae note that the dataset used for the weighting in cartogram_cont() is not particularly significant, just a proof of concept at this stage:
[R Graph Gallery][1]
[1]: https://www.r-graph-gallery.com/331-basic-cartogram/
Shapefile from: [Lle Shapefile Location][2]
[2]: http://lle.gov.wales/catalogue/item/LocalAuthorities/?lang=en
library(dplyr)
library(leaflet)
library(maptools)
library(cartogram)
library(devtools)
install_github("HanOostdijk/odataR" , build_vignettes = T)
library(odataR)
library(tidyr)
library(rgdal)
library(htmltools)
#Read in shapefile and transform shape
#dsn = folder name, layer = filename but drop the .shp
shapefile <- readOGR(dsn = "Wales Shapefile",
layer = "localauthoritiesPolygon") %>%
#Transform coordinate referencing system
spTransform(CRS("+init=epsg:4326"))
#Next step is to join an interesting dataset to the shapefile using dplyr, then pass this to the cartoram package to render.
#Gone for the teacher sickness dataset from Stats Wales. Noticed it's only up to 2017, wonder if they've stopped collecting.
teacher_sickness_data <- odataR_query('http://open.statswales.gov.wales/dataset/schw0001')
#Check values for join.
categories <- unique(teacher_sickness_data$Area_ItemName_ENG)
categories_shp <- shapefile#data$name_en
categories
categories_shp
#Teacher data has "All Welsh local authorities". Not contained in shapefile so remove.
UA_sickness_data <- teacher_sickness_data[-c(2, 4:6, 8, 9, 11:13, 15:17)] %>%
filter(Area_ItemName_ENG != "All Welsh local authorities")
#Perform join to shapefile
shapefile_1 <- shapefile %>%
merge(UA_sickness_data, by.x = "name_en", by.y = "Area_ItemName_ENG",
duplicateGeoms = TRUE)
#Shiny App will allow choice of inputs to achieve one row per polygon. However, for testing
#functionality with cartograph functions, perform test filtering.
data_filtered <- UA_sickness_data %>%
filter(Year_ItemName_ENG == 2017) %>%
filter(Type_ItemName_ENG == "Full-time") %>%
filter(Variable_ItemName_ENG == "Total days of sick leave")
test_merge <- shapefile %>%
merge(data_filtered, by.x = "name_en", by.y = "Area_ItemName_ENG")
nc_pal <- colorNumeric(palette = "Reds",
domain = log(test_merge#data$Data))
m <-test_merge %>%
leaflet() %>%
addProviderTiles(providers$CartoDB.Positron) %>%
addPolygons(weight = 1,
color = ~nc_pal(log(Data)),
label = ~name_en,
highlight = highlightOptions(weight = 3,
color = "crimson",
bringToFront = TRUE),
popup = ~ paste0(Variable_ItemName_ENG, "<br/>",
"<b/>",
Data))
m
wales_cart <- cartogram_cont(test_merge, "Data", itermax=5)
plot(wales_cart)
[![Wales_Cartogram][3]][3]
[3]: https://i.stack.imgur.com/2tsMC.png
library(tidyverse)
library(ggmap)
library(broom)
library(rgeos) #used for gBuffer
#Buffer allows to tidy cartogram based on factor of choice.
wales_cart_buffered <- gBuffer(wales_cart, byid=TRUE, width=0)
#tidy cartogram in order to pass to ggplot
spdf_fortified_wales <- tidy(wales_cart_buffered, region = "name_en")
#Now perform a join based on english UA names
spdf_fortified_wales_joined <- spdf_fortified_wales %>%
left_join(. , wales_cart#data, by=c("id"="name_en"))
ggplot() +
geom_polygon(data = spdf_fortified_wales_joined, aes(fill = Data, x = long, y = lat, group = "name_en") , size=0, alpha=0.9) +
coord_map() +
theme_void()
[![incorrect_ggplot][4]][4]
[4]: https://i.stack.imgur.com/as0Z4.png
ggplot() +
geom_polygon(data = spdf_fortified_wales_joined, aes(fill = Data, x = long, y = lat, group = "name_en") , size=0, alpha=0.9) +
coord_map() +
theme_void()
Success Criteria: Polygons are rendered correctly distorted and colour scale reflects weighting variable.
I'm trying to color Arizona, Utah, and Idaho by different colors. Ideally I'd use a color gradient to color them by a variable I choose. But I can't seem to find any other information on the web about doing this.
This is the code I have so far:
library(ggplot2)
ggplot(data = azutid) +
geom_polygon(aes(x = long, y = lat, group = group), fill = "green", color = "black") +
coord_fixed(1.3) +
guides(fill = FALSE)
I imported the map and regions from the basic "maps" package. Thanks!
It can be done easily through the use of leaflet package. Although you will have to download the census data from
https://www.census.gov/geo/maps-data/data/cbf/cbf_state.html
You can also add costum popups and highlight on hover labels to the map.
library(leaflet)
library(spdplyr)
library(rgdal)
states <- readOGR(dsn = "./cb_2016_us_state_20m/cb_2016_us_state_20m.shp",
layer = "cb_2016_us_state_20m", verbose = FALSE)
group1 <- c("AZ","UT","ID")
newobj <- states %>%
filter(as.character(STUSPS) %in% group1)
m <- leaflet(newobj) %>%
setView(-96, 37.8, 4) %>% addProviderTiles("CartoDB.Positron")
pal = colorQuantile("YlOrRd", domain = newobj$value, n=7)
m %>% addPolygons(fillColor = "orange",
weight = 2,
opacity = 1,
color = "white",
dashArray = "3",
fillOpacity = 0.7)
I have created a map using leaflet in R. It is a map of the United States with polygons being zip code level. I want to color the zip codes using a continuous color palette based on some value. I have followed the example here and have successfully mapped each zip code with a continuous color using the colorNumeric function like this:
# Create a continuous palette function
library(leaflet)
library(rgdal)
library(dplyr)
# From https://raw.githubusercontent.com/datasets/geo-boundaries-world-110m/master/countries.geojson
countries <- readOGR("json/countries.geojson", "OGRGeoJSON")
map <- leaflet(countries)
pal <- colorNumeric(palette = colorRamp(c('#4575B4', '#D73027', '#FFFFBF'), interpolate="linear"),
domain = countries$gdp_md_est)
map %>%
addPolygons(stroke = FALSE, smoothFactor = 0.2, fillOpacity = 1,
color = ~pal(gdp_md_est))
What makes this complicated is that now I want to separately color zip codes (basically replacing the previous color) using a discrete color palette. As a simple example, I want to use the colorNumeric function above to color each zip code based on average weight. Then I would use another zip code level field in my data to make all zip codes' polygons black if I wanted to exclude it for some reason, otherwise it would leave the colors as they already are.
I have found it difficult to use leaflet and colorNumeric to achieve both continuous and discrete coloring of my map. Any help would be greatly appreciated!
Since the sample above is not enough to have a demonstration, I decided to use one of the dummy data that I used for other leaflet related questions. I hope you do not mind that. Given what you said, you need to create two layers in your map. One for a continuous variable, and the other for a discrete variable. This means that you need to create two sets of colors. As you used, you want to use colorNumeric() for the continuous variable. You want to use colorFactor() for the discrete variable. In my sample code, I create a new discrete variable called group. Once you finish creating the color palettes, you want to draw a map. You need to use addPolygons() twice. Make sure that you use group. This is going to appear in the layer control button on the right upper corner. As far as I know, we cannot display one legend only at the moment. I came across this issue before and concluded that we have no choice at the moment. I hope this demonstration is enough for you to make a progress in your task.
library(raster)
library(dplyr)
library(leaflet)
# Get UK polygon data
UK <- getData("GADM", country = "GB", level = 2)
### Create dummy data
set.seed(111)
mydf <- data.frame(place = unique(UK$NAME_2),
value = sample.int(n = 1000, size = n_distinct(UK$NAME_2), replace = TRUE))
### Create a new dummy column for a discrete variable.
mydf <- mutate(mydf, group = cut(value, breaks = c(0, 200, 400, 600, 800, 1000),
labels = c("a", "b", "c", "d", "e"),
include.lowest = TRUE))
### Create colors for the continuous variable (i.e., value) and the discrete variable.
conpal <- colorNumeric(palette = "Blues", domain = mydf$value, na.color = "black")
dispal <- colorFactor("Spectral", domain = mydf$group, na.color = "black")
leaflet() %>%
addProviderTiles("OpenStreetMap.Mapnik") %>%
setView(lat = 55, lng = -3, zoom = 6) %>%
addPolygons(data = UK, group = "continuous",
stroke = FALSE, smoothFactor = 0.2, fillOpacity = 0.3,
fillColor = ~conpal(mydf$value),
popup = paste("Region: ", UK$NAME_2, "<br>",
"Value: ", mydf$value, "<br>")) %>%
addPolygons(data = UK, group = "discrete",
stroke = FALSE, smoothFactor = 0.2, fillOpacity = 0.3,
fillColor = ~dispal(mydf$group),
popup = paste("Region: ", UK$NAME_2, "<br>",
"Value: ", mydf$group, "<br>")) %>%
addLayersControl(overlayGroups = c("continuous", "discrete")) %>%
addLegend(position = "bottomright", pal = conpal, values = mydf$value,
title = "UK value",
opacity = 0.3) %>%
addLegend(position = "bottomleft", pal = dispal, values = mydf$group,
title = "UK group",
opacity = 0.3)
If you choose the continuous-variable layer, you will see the following map.
If you choose the discrete-variable layer, you will see the following map.
Update
If you want to show both a continuous group and a continuous group together, you need to subset your data beforehand so that there is no overlapping in polygons. Using UK and mydf above, you can try something like this.
### Subset data and create two groups. This is something you gotta do
### in your own way given I have no idea of your own data.
con.group <- mydf[1:96, ]
dis.group <- mydf[97:192, ]
### Create colors for the continuous variable (i.e., value) and the discrete variable.
conpal <- colorNumeric(palette = "Blues", domain = c(min(mydf$value), max(mydf$value)), na.color = "black")
dispal <- colorFactor(palette = "Reds", "Spectral", levels = unique(mydf$group), na.color = "black")
### Subset the polygon data as well
con.poly <- subset(UK, NAME_2 %in% con.group$place)
dis.poly <- subset(UK, NAME_2 %in% dis.group$place)
leaflet() %>%
addProviderTiles("OpenStreetMap.Mapnik") %>%
setView(lat = 55, lng = -3, zoom = 6) %>%
addPolygons(data = con.poly, group = "continuous",
stroke = FALSE, smoothFactor = 0.2, fillOpacity = 0.3,
fillColor = ~conpal(con.group$value),
popup = paste("Region: ", UK$NAME_2, "<br>",
"Value: ", con.group$value, "<br>")) %>%
addPolygons(data = dis.poly, group = "discrete",
stroke = FALSE, smoothFactor = 0.2, fillOpacity = 0.3,
fillColor = ~dispal(dis.group$group),
popup = paste("Region: ", UK$NAME_2, "<br>",
"Group: ", dis.group$group, "<br>")) %>%
addLayersControl(overlayGroups = c("continuous", "discrete")) %>%
addLegend(position = "bottomright", pal = conpal, values = con.group$value,
title = "UK value",
opacity = 0.3) %>%
addLegend(position = "bottomleft", pal = dispal, values = dis.group$group,
title = "UK group",
opacity = 0.3)