I want to make a Shiny app where the colouring of a choropleth is based on a numeric value of one of many possible quantitative variables that a user can select from. In simple cases, this is straightforward, but I'm unsure of the best practices when we have 20+ variables, with quite detailed shape files (~2300 polygons).
It might or might not be relevant that the variables might be completely independent to each other such as 'Total Population' or 'Average Temperature' but some of them will have a temporal relationship such as 'Total Population' at 3 or more points in time.
One of the main shapefiles I am using is the ABS Statistical Area 2. Below I give the population density (total population/area) for Australia and a zoomed in view of Sydney to better convey the level of detail I'm interested in.
Australia Sydney
I have read the shapefile in to R and greatly reduced the complexity/number of points using the ms_simplify()
function in the rmapshaper
package.
Now as far as Shiny and leaflet go, this is what I have been doing:
Before the server
object is defined in server.R
, I build a primary map object with all the desired 'layers'. That is, a leaflet with numerous addPolygon()
calls to define the colouring of each 'layer' (group).
# Create main map
primary_map <- leaflet() %>%
addProviderTiles(
providers$OpenStreetMap.BlackAndWhite,
options = providerTileOptions(opacity = 0.60)
) %>%
# Layer 0 (blank)
addPolygons(
data = aus_sa2_areas,
group = "blank"
) %>%
# Layer 1
addPolygons(
data = aus_sa2_areas,
fillColor = ~palette_layer_1(aus_sa2_areas$var_1),
smoothFactor = 0.5,
group = "layer_1"
) %>%
...
# Layer N
addPolygons(
data = aus_sa2_areas,
fillColor = ~palette_layer_n(aus_sa2_areas$var_n),
smoothFactor = 0.5,
group = "layer_n"
) %>% ...
All bar the first layer is then hidden using hideGroup()
so that the initial rendering of the map doesn't look silly.
hideGroup("layer_1") %>%
hideGroup("layer_2") %>%
...
hideGroup("layer_n")
In the Shiny app, using radio buttons (layer_selection
), the user can select the 'layer' they'd like to see. I use observeEvent(input$layer_selection, {})
to watch the status of the radio button options.
To update the plot, I use leafletProxy()
and hideGroup()
to hide all the groups and then showGroup()
to unhide the selected layer.
I apologize for the lack of reproducible example.
Questions
How can I optimise my code? I am eager to make it more performant and/or easy to work with. I've found using hideGroup()
's/showGroup()
for each layer selection is far faster than using addPolygon()
to a blank map, but this causes the app to take a very significant amount of time to load.
Can I change the variable I am colouring the polygons by, without redrawing or adding those polygons again? To clarify, if I have 2 different variables to plot, both using the same shape data, do I have to do 2 distinct addPolygon()
calls?
Is there a more automatic way to sensibly colour the polygons for each layer according to a desired palette (from the viridis package?). Right now I'm finding defining a new palette for each variable, rather cumbersome, eg:
palette_layer_n <- colorNumeric(
palette = "viridis",
domain = aus_sa2_areas$aus_sa2_areas$var_n
)
Side Question
How does this map on the ABS website work? It can be incredibly detailed and yet extremely responsive. Compare the Mesh Block detail to the SA2 (2310 polygons), example below:
Since you haven't gotten any answers yet, I'll post a few things that can maybe help you, based on a simple example.
It would of course be easier if yours was reproducible; and I suppose from looking around you have already seen that there are several related issues / requests (about re-coloring polygons), whereas it doesn't seem that a real solution has made it into any release (of leaflet) yet.
With the below work-around you should be able to avoid multiple addPolygons
and can cover an arbitrary number of variables (for now I have just hard-coded a single variable into the modFillCol
call though).
library(leaflet)
library(maps)
library(viridis)
mapStates = map("state", fill = TRUE, plot = FALSE)
# regarding Question 3 - the way you set the domain it looks equivalent
# to just not setting it up front, i.e. domain = NULL
myPalette <- colorNumeric(
palette = "viridis",
domain = NULL
)
mp <- leaflet(data = mapStates) %>%
addTiles() %>%
addPolygons(fillColor = topo.colors(10, alpha = NULL), stroke = FALSE)
# utility function to change fill color
modFillCol <- function(x, var_x) {
cls <- lapply(x$x$calls, function(cl) {
if (cl$method == "addPolygons") {
cl$args[[4]]$fillColor <- myPalette(var_x)
}
cl
})
x$x$calls <- cls
x
}
# modify fill color depending on the variable, in this simple example
# I just use the number of characters of the state-names
mp %>%
modFillCol(nchar(mapStates$names))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With