using shiny to map zip codes as a choropleth?

1,319 views
Skip to first unread message

arila...@gmail.com

unread,
Aug 22, 2014, 9:50:22 PM8/22/14
to shiny-...@googlegroups.com
I really like Joe's "Superzip" example:


It renders zips as a graduated symbol map - the zips are circles, and their size indicates a value. I am interested in making a variation on this project where instead of rendering zips as a graduated symbol, they are rendered as a choropleth. That is, the boundaries are drawn and then then filled with a color that represents their value.

I do not have much prior experience with javascript mapping libraries, but I see that mbostock has a TopoJSON map of US zip code boundaries here:


Is it possible to modify the Superzip project so that it renders these boundaries on top of the US map, and then fill them in with colors based on my own criteria?

Thanks.

Ari

Franco Peschiera

unread,
Aug 23, 2014, 3:29:23 AM8/23/14
to shiny-...@googlegroups.com

arila...@gmail.com

unread,
Aug 23, 2014, 2:10:10 PM8/23/14
to shiny-...@googlegroups.com
Thanks Franco.

It looks like this example uses the map package. It looks like maps::map only works for shapefiles that the map package ships with, which does not include zip/zcta boundary files.  My concern is that even if I could get it to work with the zcta shapefile, that path is not computationally tractable.  For example, in February I posted a question about that to the r-sig-geo mailing list. The ZCTA boundaries from the census are so large that it seems like that path just doesn't finish.

This is why I was wondering if it's possible to to render Mike Bostock's topojson/geojson boundaries for zip codes (which I assume are the same 2010 census zcta boundaries I was using before) with R and leaflet.  

Ari

Franco Peschiera

unread,
Aug 27, 2014, 4:43:37 AM8/27/14
to shiny-...@googlegroups.com
I'm not sure about topojson but the rCharts package can use geojson to populate leaflet maps as shown in this example.
From my experience, using rCharts and geojson with highly detailed polygons was very slow. I ended up "cutting" the polygons to get less detailed ones so it could low faster (by getting only 1 coordinate every 30 coordinates in the sequence, for example).

hope this helps.

Franco

Chris Holcomb

unread,
Aug 27, 2014, 3:54:21 PM8/27/14
to shiny-...@googlegroups.com
As far as creating the data in a suitable format for leaflet-shiny, I couldn't quickly get it to work but I'm fairly confident you can do it without "fortify". Perhaps convert to SpatialPolygonsDataFrame or geojson and parse that. I suspect my attempt failed for this data because some are multipolygons and shiny-leaflet does not support those. You would need to combine them into polygons or keep them separate and tie together those id's to their proper information. Perhaps your "fortify" attempt to change the format would succeed if you first simplified the polygons, spilt or union-ed them (I'd suggest Postgis).

However, I suspect Franco is right and this is going to very slow with leaflet-shiny or rMaps, because neither support topojson AFAIK. I actually think you're best bet is to stitch together a HTML UI and use full Leaflet (or OpenLayers3) with raster layers, topojson, multipolygons, layergroups, etc, which is what I'll be doing unless I can (doubtfully) contribute to leaflet-shiny or if these features are included soon.

Another option is to just not show that much data. With leaflet-shiny you can pull the map zoom level and extents. What if you only show state polygons until you zoom in and then show the ZCTAs?

-C

arila...@gmail.com

unread,
Aug 28, 2014, 1:24:57 PM8/28/14
to shiny-...@googlegroups.com

On Wednesday, August 27, 2014 12:54:21 PM UTC-7, Chris Holcomb wrote:

Another option is to just not show that much data. With leaflet-shiny you can pull the map zoom level and extents. What if you only show state polygons until you zoom in and then show the ZCTAs?

-C

We would be very happy to do that. For this project, the biggest bang for the buck is visualizing how business metrics differ across zips in a single city. Or comparing how (at a zip level) suburbs of a city compare with the city center. 



 
On Wednesday, August 27, 2014 3:43:37 AM UTC-5, Franco Peschiera wrote:
I'm not sure about topojson but the rCharts package can use geojson to populate leaflet maps as shown in this example.
From my experience, using rCharts and geojson with highly detailed polygons was very slow. I ended up "cutting" the polygons to get less detailed ones so it could low faster (by getting only 1 coordinate every 30 coordinates in the sequence, for example).

hope this helps.

Franco

El sábado, 23 de agosto de 2014 20:10:10 UTC+2, arila...@gmail.com escribió:
Thanks Franco.

It looks like this example uses the map package. It looks like maps::map only works for shapefiles that the map package ships with, which does not include zip/zcta boundary files.  My concern is that even if I could get it to work with the zcta shapefile, that path is not computationally tractable.  For example, in February I posted a question about that to the r-sig-geo mailing list. The ZCTA boundaries from the census are so large that it seems like that path just doesn't finish.

This is why I was wondering if it's possible to to render Mike Bostock's topojson/geojson boundaries for zip codes (which I assume are the same 2010 census zcta boundaries I was using before) with R and leaflet.  

Ari

On Saturday, August 23, 2014 12:29:23 AM UTC-7, Franco Peschiera wrote:
Take a look at Joe's choropleth example: https://groups.google.com/forum/m/#!msg/shiny-discuss/BzbHBID_7Hk/4S_eBX7T5PIJ

Franco Peschiera

unread,
Aug 28, 2014, 1:45:40 PM8/28/14
to arila...@gmail.com, shiny-discuss
I am also interested in playing with the zoom level to control what actually gets shown int he maps: is there an example of leaflet-shiny working with the zoom level to change the visualization?

thanks!

Franco
--
Sent from my netBook


--
You received this message because you are subscribed to a topic in the Google Groups "Shiny - Web Framework for R" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/shiny-discuss/MBsVWgC6E8s/unsubscribe.
To unsubscribe from this group and all its topics, send an email to shiny-discus...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/shiny-discuss/a4dbde33-e696-41d5-9fa3-8276564da7ae%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Chris Holcomb

unread,
Aug 29, 2014, 5:09:26 PM8/29/14
to shiny-...@googlegroups.com
First, with the original question, a better way to get a file suitable file for leaflet-shiny. Here's how you can parse a SpatialDataFrame. NOTE: Before you do this you'll need to convert it to Polygons from MultiPolygons and simplify the files. I noticed that the maps in the maps package have far fewer lat-longs than raw CENSUS shapefiles read into SpatialDataFrames. I highly recommend PostGIS, specifically ST_Simplify, ST_GeometryN, ST_Union, ST_Dump, ST_MakeValid, and ST_SnapToGrid. Try this out on ZCTAs and let us know how it goes, it takes very little time on counties that have been previously simplified, but I'm unsure about ZCTAs.

library(sp);library(maptools);library(maps)
getMap  <- function(sdf) {
  getLatLongs <- function(polygon) {
    currentPolygon <- slot(polygon,"Polygons")
    # only single Polygons
    x <- slot(currentPolygon[[1]],"coords")[,1]
    y <- slot(currentPolygon[[1]],"coords")[,2]
    return(list("x"=c(x,NA),"y"=c(y,NA)))
  }
  allLatLongs <- do.call(rbind.data.frame, lapply(slot(sdf, "polygons"), function(x) getLatLongs(x)))
  # may need to change NAME to what you want to display  
  names <- as.vector(slot(sdf,"data")$NAME)
  # there's probably a better way to handle the inserted NAs for calculating range
  range <- c(range(allLatLongs$x[!is.na(allLatLongs$x)]),range(allLatLongs$y[!is.na(allLatLongs$y)]))
  prepped <- list("x"=allLatLongs$x,"y"=allLatLongs$y,"range"=range,"names"=names)
  attr(prepped , "class") <- "map"
  return(prepped)
}
# set Proj4 to correct string via spatialreference.org or prj2epsg.org's API + PostGIS's spatial_ref_sys
sdfStates <- readShapeSpatial('tl_2013_us_state.shp',proj4string = CRS(Proj4))
saveRDS(getMap(sdfStates),'states.rds')

Now what you'll also really want to do to make the following example complete is to calculate the range of each of the polygons so you can later subset the displayed polygons to the polygons within the viewable map. I have not done this yet, but one method would be to calculate bounds for each polygon and save that as a separate data.frame. Then you would calculate a buffer around input$map_bounds, subset the aforementioned data.frame and let the program reactively run the getMap function, which I imagine will be quick for smaller numbers of polygons. A cleaner route would be to let R access the simplied polygons loaded in PostGIS and conduct a ST_Intersects join with a buffered geometry defined by input$map_bounds. If someone gets around to this, please let me know how fast it is for huge sets of polygons like the ZCTAs.

Anyways, this example shows you how to do the changing polygons by input$map_zoom part:


ui.R
library(shiny);library(leaflet)
shinyUI(navbarPage("Zoom Changing Maps",
   tabPanel("Map",
          leafletMap("map", "100%", 900,
                     options=list(center = c(40, -98.85),zoom = 5)))))
server.R
library(shiny);library(leaflet);library(maps);library(RColorBrewer)
states <- map("state", plot=FALSE, fill=TRUE)
counties <- map("county", plot=FALSE, fill=TRUE)
shinyServer(function(input, output, session) {
  map <- createLeafletMap(session, "map")
  output$map <- reactive(TRUE)
  zoomLevel <- reactive({
      zoom <- ifelse(is.null(input$map_zoom),5,input$map_zoom)
      zoomLevel <-  ifelse(zoom > 6,"counties","states")
      return(zoomLevel)})
  mapData <- reactive({
    zoomLevel()
    isolate({
      if(zoomLevel()=="counties") {mapData <- counties} else {mapData <- states}
      return(mapData)})})
  observe({
    mapData <- mapData()
    isolate({session$onFlushed(once=TRUE, function() {
      map$clearShapes()
      map$addPolygon(as.numeric(mapData$y),as.numeric(mapData$x), mapData$names,
             lapply(brewer.pal(9, "Blues"), function(x) {
               list(fillColor = x)
             }),
             list(fill=TRUE, fillOpacity=.5, 
                  stroke=TRUE, opacity=1, color="blue", weight=1
             ))})})})})

Chris Holcomb

unread,
Aug 29, 2014, 6:25:03 PM8/29/14
to shiny-...@googlegroups.com
Here's how to pull the bounding boxes from a spatialdataframe into a dataframe:

getBBOXES <- function(sdf){
  getBBOX <- function(polygon) {
    currentBBOX <- bbox(polygon)
    return(data.frame("minLat"=currentBBOX[2],"maxLat"=currentBBOX[4],"minLng"=currentBBOX[1],"maxLng"=currentBBOX[3]))
  }
  allBBOX <- do.call(rbind.data.frame, lapply(slot(sdf, "polygons"), function(x) getBBOX(x)))
  allBBOX$names <- slot(sdf,"data")$NAME
  return(allBBOX)
}

On Friday, August 22, 2014 8:50:22 PM UTC-5, arila...@gmail.com wrote:
Reply all
Reply to author
Forward
0 new messages