library(tidyverse)
library(sf)
library(sp)
library(tmap)
library(osmdata)
library(ggmap)
library(readxl)
library(classInt)
library(cowplot)
library(maps)
library(ggspatial)
library(basemaps)
library(mapview)
tree_shapes <- st_read("data/Parks_Tree_Inventory-shp/")
## Reading layer `fd3ad191-4bf0-4577-b6ec-724baeb619792020413-1-a3eocm.isio' from data source `C:\Users\boyernat\OneDrive - Oregon Health & Science University\Documents\2025-04 Spring Term\BMI 525 Data Visualization\Labs\08_lab\data\Parks_Tree_Inventory-shp'
## using driver `ESRI Shapefile'
## Simple feature collection with 25534 features and 39 fields
## Geometry type: POINT
## Dimension: XY
## Bounding box: xmin: -13667450 ymin: 5692149 xmax: -13635010 ymax: 5724486
## Projected CRS: WGS 84 / Pseudo-Mercator
pdx_boundaries <- st_read("data/Neighborhoods__Regions_-shp/")
## Reading layer `Neighborhoods__Regions_' from data source
## `C:\Users\boyernat\OneDrive - Oregon Health & Science University\Documents\2025-04 Spring Term\BMI 525 Data Visualization\Labs\08_lab\data\Neighborhoods__Regions_-shp'
## using driver `ESRI Shapefile'
## Simple feature collection with 98 features and 10 fields
## Geometry type: MULTIPOLYGON
## Dimension: XY
## Bounding box: xmin: -13677560 ymin: 5689875 xmax: -13632920 ymax: 5724919
## Projected CRS: WGS 84 / Pseudo-Mercator
river_boundaries <- st_read("data/Willamette_Columbia_River_Ordinary_High_Water-shp/")
## Reading layer `Willamette_Columbia_River_Ordinary_High_Water' from data source
## `C:\Users\boyernat\OneDrive - Oregon Health & Science University\Documents\2025-04 Spring Term\BMI 525 Data Visualization\Labs\08_lab\data\Willamette_Columbia_River_Ordinary_High_Water-shp'
## using driver `ESRI Shapefile'
## Simple feature collection with 1 feature and 5 fields
## Geometry type: MULTIPOLYGON
## Dimension: XY
## Bounding box: xmin: -13676470 ymin: 5686111 xmax: -13632810 ymax: 5727803
## Projected CRS: WGS 84 / Pseudo-Mercator
Map centered on your neighborhood!
pdx_boundaries %>%
ggplot() +
geom_sf() +
coord_sf(
xlim=c(-13658000,-13650000),
ylim=c(5699500, 5707500)) +
geom_sf_label(aes(label = MAPLABEL), size = 3) +
geom_sf_label(data = pdx_boundaries %>% filter(MAPLABEL == "Buckman Community Association"),
aes(label = MAPLABEL,
fill = "pink"),
size = 3) +
theme(legend.position = "none")
Changing river and neighborhood color!
First, simplify river points for quicker plotting.
river_boundaries <- river_boundaries %>% st_simplify(dTolerance = 10)
pdx_boundaries %>%
ggplot() +
geom_sf(fill = "palegreen1",
col = "black") +
geom_sf(data = river_boundaries,
fill = "lightblue1",
col = "midnightblue",
size = 0.0) +
theme_classic()
Experiment with projections!
# world map
world <- st_as_sf(map('world', plot = FALSE, fill = TRUE))
New projection: “August Epicycloidal” Attempting to center on the US – can see that this projection creates some major distortion for areas on opposite side of the globe from the US.
world2 <- st_transform(
world,
"+proj=august +ellps=WGS84 +over +lat_1=29.5 +lat_2=45.5 +lat_0=37.5 +lon_0=-96 +x_0=0 +y_0=0"
)
ggplot() +
geom_sf(data = world2)
## Warning in CPL_transform(x, crs, aoi, pipeline, reverse, desired_accuracy, :
## GDAL Error 1: PROJ: pipeline: Pipeline: Inverse operation for august is not
## available
Now, the coordinates are flipped (opposite sign) in an attempt to focus of the other side of the world. A similar problem persists.
world3 <- st_transform(
world,
"+proj=august +ellps=WGS84 +over +lat_1=-29.5 +lat_2=-45.5 +lat_0=-37.5 +lon_0=96 +x_0=0 +y_0=0"
)
ggplot() +
geom_sf(data = world3)
## Warning in CPL_transform(x, crs, aoi, pipeline, reverse, desired_accuracy, :
## GDAL Error 1: PROJ: pipeline: Pipeline: Inverse operation for august is not
## available
Plot both grocery stores and farmers markets!
Data:
farmers_market <- st_read("data/Farmers_Markets-shp/")
## Reading layer `Farmers_Markets' from data source
## `C:\Users\boyernat\OneDrive - Oregon Health & Science University\Documents\2025-04 Spring Term\BMI 525 Data Visualization\Labs\08_lab\data\Farmers_Markets-shp'
## using driver `ESRI Shapefile'
## Simple feature collection with 20 features and 11 fields
## Geometry type: POINT
## Dimension: XY
## Bounding box: xmin: -13665000 ymin: 5696460 xmax: -13644270 ymax: 5714893
## Projected CRS: WGS 84 / Pseudo-Mercator
grocery_stores <- st_read("data/Grocery_Stores-shp/")
## Reading layer `Grocery_Stores' from data source
## `C:\Users\boyernat\OneDrive - Oregon Health & Science University\Documents\2025-04 Spring Term\BMI 525 Data Visualization\Labs\08_lab\data\Grocery_Stores-shp'
## using driver `ESRI Shapefile'
## Simple feature collection with 123 features and 9 fields
## Geometry type: POINT
## Dimension: XY
## Bounding box: xmin: -13665520 ymin: 5686206 xmax: -13634020 ymax: 5718088
## Projected CRS: WGS 84 / Pseudo-Mercator
Plot:
pdx_boundaries %>%
ggplot() +
geom_sf(fill = "white", col = 'black') +
geom_sf(data = river_boundaries,
fill = "lightblue1", size = 0.0, color = 'midnightblue') +
geom_sf(data = farmers_market,
mapping = aes(color = Day),
shape = 8) +
labs(color = "Farmer's Markets Days Open") +
scale_color_brewer(palette = "Dark2") +
ggnewscale::new_scale_color() +
geom_sf(data = grocery_stores,
mapping = aes(color = TYPE, shape = TYPE)) +
labs(shape = "Grocery Store Type",
color = "Grocery Store Type") +
scale_color_manual(values = c('Large Chain Grocery' = 'darkblue', 'Independent or Ethnic Grocery' = 'red3')) +
theme_classic()
Plot your neighborhood’s park
Getting park coordinates with drawing box help.
laurelhurst_shape <- basemaps::draw_ext()
## Loading required namespace: leaflet.extras
## Loading required package: shiny
##
## Listening on http://127.0.0.1:4466
# check basemap's coordinate system
st_crs(laurelhurst_shape)
## Coordinate Reference System:
## User input: EPSG:4326
## wkt:
## GEOGCRS["WGS 84",
## ENSEMBLE["World Geodetic System 1984 ensemble",
## MEMBER["World Geodetic System 1984 (Transit)"],
## MEMBER["World Geodetic System 1984 (G730)"],
## MEMBER["World Geodetic System 1984 (G873)"],
## MEMBER["World Geodetic System 1984 (G1150)"],
## MEMBER["World Geodetic System 1984 (G1674)"],
## MEMBER["World Geodetic System 1984 (G1762)"],
## MEMBER["World Geodetic System 1984 (G2139)"],
## MEMBER["World Geodetic System 1984 (G2296)"],
## ELLIPSOID["WGS 84",6378137,298.257223563,
## LENGTHUNIT["metre",1]],
## ENSEMBLEACCURACY[2.0]],
## PRIMEM["Greenwich",0,
## ANGLEUNIT["degree",0.0174532925199433]],
## CS[ellipsoidal,2],
## AXIS["geodetic latitude (Lat)",north,
## ORDER[1],
## ANGLEUNIT["degree",0.0174532925199433]],
## AXIS["geodetic longitude (Lon)",east,
## ORDER[2],
## ANGLEUNIT["degree",0.0174532925199433]],
## USAGE[
## SCOPE["Horizontal component of 3D system."],
## AREA["World."],
## BBOX[-90,-180,90,180]],
## ID["EPSG",4326]]
# check pdx's coordinate system
st_crs(pdx_boundaries)
## Coordinate Reference System:
## User input: WGS 84 / Pseudo-Mercator
## wkt:
## PROJCRS["WGS 84 / Pseudo-Mercator",
## BASEGEOGCRS["WGS 84",
## ENSEMBLE["World Geodetic System 1984 ensemble",
## MEMBER["World Geodetic System 1984 (Transit)"],
## MEMBER["World Geodetic System 1984 (G730)"],
## MEMBER["World Geodetic System 1984 (G873)"],
## MEMBER["World Geodetic System 1984 (G1150)"],
## MEMBER["World Geodetic System 1984 (G1674)"],
## MEMBER["World Geodetic System 1984 (G1762)"],
## MEMBER["World Geodetic System 1984 (G2139)"],
## MEMBER["World Geodetic System 1984 (G2296)"],
## ELLIPSOID["WGS 84",6378137,298.257223563,
## LENGTHUNIT["metre",1]],
## ENSEMBLEACCURACY[2.0]],
## PRIMEM["Greenwich",0,
## ANGLEUNIT["degree",0.0174532925199433]],
## ID["EPSG",4326]],
## CONVERSION["Popular Visualisation Pseudo-Mercator",
## METHOD["Popular Visualisation Pseudo Mercator",
## ID["EPSG",1024]],
## PARAMETER["Latitude of natural origin",0,
## ANGLEUNIT["degree",0.0174532925199433],
## ID["EPSG",8801]],
## PARAMETER["Longitude of natural origin",0,
## ANGLEUNIT["degree",0.0174532925199433],
## ID["EPSG",8802]],
## PARAMETER["False easting",0,
## LENGTHUNIT["metre",1],
## ID["EPSG",8806]],
## PARAMETER["False northing",0,
## LENGTHUNIT["metre",1],
## ID["EPSG",8807]]],
## CS[Cartesian,2],
## AXIS["easting (X)",east,
## ORDER[1],
## LENGTHUNIT["metre",1]],
## AXIS["northing (Y)",north,
## ORDER[2],
## LENGTHUNIT["metre",1]],
## USAGE[
## SCOPE["Web mapping and visualisation."],
## AREA["World between 85.06°S and 85.06°N."],
## BBOX[-85.06,-180,85.06,180]],
## ID["EPSG",3857]]
# transform laurelhurst coords to pdx's coord
laurelhurst_tranformed <- st_transform(laurelhurst_shape, st_crs(pdx_boundaries))
# get bounding box from laurelhurst
laurelhurst_coords <- st_bbox(laurelhurst_tranformed)
# bounding box with default stadia map coords
laurelhurst_coords2 <- st_bbox(st_transform(laurelhurst_shape, crs = 4326))
Plotting Laurelhurst Par and its trees:
pdx_boundaries %>% ggplot() +
geom_sf() +
geom_sf(data = tree_shapes,
mapping = aes(size = CrownWidth,
color = as.numeric(as.factor(Species))),
shape = 8) +
scale_color_gradientn(colors = c("#d9f0a3", "#78c679", "#238443", "#004529")) +
guides(color = FALSE,
size = FALSE) +
scale_size(range = c(0.05, 2)) +
coord_sf(xlim = laurelhurst_coords[c("xmin", "xmax")],
ylim = laurelhurst_coords[c("ymin", "ymax")]) +
theme_classic() +
labs(title = "Laurelhurst Park Tree Inventory",
subtitle = "Trees colored by species, size indicative of crown width")
Use OpenStreetMap tiles
# stadia map on Laurelhurst park
laurelhurst_basemap <- get_stadiamap(c(left=-122.63245, bottom=45.51824, right=-122.62059, top=45.52357),
zoom = 16,
maptype="stamen_terrain")
## ℹ © Stadia Maps © Stamen Design © OpenMapTiles © OpenStreetMap contributors.
# Tree map coords tranformed for stadia
trees_transformed <- st_transform(tree_shapes, st_crs(4326))
ggmap(laurelhurst_basemap) +
geom_sf(data = trees_transformed,
inherit.aes = FALSE,
mapping = aes(size = CrownWidth,
color = as.numeric(as.factor(Species))),
shape = 8) +
scale_color_gradientn(colors = c("#d9f0a3", "#78c679", "#238443", "#004529")) +
guides(color = FALSE,
size = FALSE) +
scale_size(range = c(0.05, 2)) +
theme_classic() +
labs(title = "Laurelhurst Park Tree Inventory",
subtitle = "Trees colored by species, size indicative of crown width",
y = "Latitude",
x = "Longitude")
## Coordinate system already present. Adding new coordinate system, which will
## replace the existing one.
## Warning: Removed 263 rows containing missing values or values outside the scale range
## (`geom_sf()`).
A map using a different census column (or multiple columns!)
# read in census data, selecting up until desired column and rows
pdx_population <- read_excel("data/Census_2010_Data_Cleanedup.xlsx",
sheet="Census_2010_Neighborhoods",
range="A7:CM101",
col_names = FALSE)
# columns to keep
c1 <- 1
c2 <- ncol(pdx_population) - 3
c3 <- ncol(pdx_population)
keep_cols <- c(c1, c2, c3)
# subset columns of interest (first = neighborhoods, last = renters)
pdx_population <- pdx_population[, keep_cols] %>%
rename("Neighborhood" = "...1", "rentals" = "...91", "total_homes" = "...88") %>%
mutate(Neighborhood=as.factor(Neighborhood)) %>%
mutate(rental_density = rentals / total_homes)
# we need to do a tiny bit of cleanup; the census spreadsheet has some slightly different names
pdx_population <- pdx_population %>% mutate(Neighborhood=recode(Neighborhood,
"ARGAY" = "ARGAY TERRACE",
"BROOKLYN" = "BROOKLYN ACTION CORPS",
"BUCKMAN" = "BUCKMAN COMMUNITY ASSOCIATION",
"CENTENNIAL" = "CENTENNIAL COMMUNITY ASSOCIATION",
"CULLY" = "CULLY ASSOCIATION OF NEIGHBORS",
"CENTENNIAL" = "CENTENNIAL COMMUNITY ASSOCIATION",
"DOWNTOWN" = "PORTLAND DOWNTOWN",
"GOOSE HOLLOW" = "GOOSE HOLLOW FOOTHILLS LEAGUE",
"HAYDEN ISLAND" = "HAYDEN ISLAND NEIGHBORHOOD NETWORK",
"HOSFORD-ABERNETHY" = "HOSFORD-ABERNETHY NEIGHBORHOOD DISTRICT ASSN.",
"IRVINGTON" = "IRVINGTON COMMUNITY ASSOCIATION",
"LLOYD DISTRICT" = "LLOYD DISTRICT COMMUNITY ASSOCIATION",
"NORTHWEST DISTRICT" = "NORTHWEST DISTRICT ASSOCIATION",
"OLD TOWN-CHINATOWN" = "OLD TOWN COMMUNITY ASSOCIATION",
"PARKROSE HEIGHTS" = "PARKROSE HEIGHTS ASSOCIATION OF NEIGHBORS",
"PEARL" = "PEARL DISTRICT",
"SABIN" = "SABIN COMMUNITY ASSOCIATION",
"SELLWOOD-MORELAND" = "SELLWOOD-MORELAND IMPROVEMENT LEAGUE",
"SOUTHWEST HILLS" = "SOUTHWEST HILLS RESIDENTIAL LEAGUE",
"SUMNER" = "SUMNER ASSOCIATION OF NEIGHBORS",
"SUNDERLAND" = "SUNDERLAND ASSOCIATION OF NEIGHBORS",
"WILKES" = "WILKES COMMUNITY GROUP"
))
# and now left join with our boundary data set- note that there will be some regions with no population data, so we want a left-join
boundaries_with_pop <- left_join(pdx_boundaries, pdx_population, by=c("NAME"="Neighborhood"))
boundaries_with_pop %>% glimpse
## Rows: 98
## Columns: 14
## $ OBJECTID <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, …
## $ NAME <chr> "CATHEDRAL PARK", "UNIVERSITY PARK", "PIEDMONT", "WOODL…
## $ COMMPLAN <chr> NA, NA, "ALBINA", "ALBINA", NA, "ALBINA", "ALBINA", "AL…
## $ SHARED <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ COALIT <chr> "NPNS", "NPNS", "NPNS", "NECN", "CNN", "NPNS", "NPNS", …
## $ HORZ_VERT <chr> "HORZ", "HORZ", "VERT", "HORZ", "HORZ", "HORZ", "HORZ",…
## $ MAPLABEL <chr> "Cathedral Park", "University Park", "Piedmont", "Woodl…
## $ ID <int> 31, 88, 70, 93, 23, 2, 66, 19, 67, 84, 44, 48, 89, 63, …
## $ Shape_Leng <dbl> 11434.255, 11950.860, 10849.327, 8078.361, 18179.392, 9…
## $ Shape_Area <dbl> 5424297.8, 6981456.8, 6079530.0, 3870553.6, 16580624.7,…
## $ total_homes <dbl> 1703, 1834, 2983, 2084, 5230, 2774, 2756, 4001, 2314, 8…
## $ rentals <dbl> 699, 617, 1030, 656, 2148, 825, 919, 1125, 1160, 222, 1…
## $ rental_density <dbl> 0.41045214, 0.33642312, 0.34528998, 0.31477927, 0.41070…
## $ geometry <MULTIPOLYGON [m]> MULTIPOLYGON (((-13664216 5..., MULTIPOLYG…
boundaries_with_pop %>%
ggplot() +
geom_sf(aes(fill = rental_density)) +
geom_sf(data = river_boundaries, fill = 'lightblue1', size = 0) +
theme_classic() +
labs(title = "Portland, OR Neighborhood Rentals",
subtitle = "Proportion of homes that are occupied by renters, by neighborhood",
y = "Latitude",
x = "Longitude",
fill = "Proportion of Rentals")
Exploring color scales
Distribution of rental proportions.
ggplot(data = boundaries_with_pop,
mapping = aes(x = reorder(NAME, -rental_density),
y= rental_density)) +
geom_col(col = "white") +
theme_classic() +
theme(axis.text.x = element_text(angle = 90,
vjust = 0.5,
hjust = 1,
size = 3)) +
labs(y = "Proportion of Rentals",
x = "Neighborhoods")
ggplot(data = boundaries_with_pop,
mapping = aes(x = rental_density)) +
geom_histogram() +
labs(x = "Proprotion of Rentals")
Different approaches to discretizing rental density.
# set up
min.rental <- min(boundaries_with_pop$rental_density, na.rm = TRUE)
max.rental <- max(boundaries_with_pop$rental_density, na.rm = TRUE)
range.rental <- max.rental - min.rental
stdev.rental <- sd(boundaries_with_pop$rental_density, na.rm = TRUE)
# Equal interval bins (bins = 7)
equal.interval <- seq(min.rental, max.rental, by = range.rental/6)
# Quantile interval bins
quantile.interval <- quantile(boundaries_with_pop$rental_density, probs=seq(0,1, by = 1/7), na.rm = TRUE)
# Standard deviation intervals
std.interval <- c(seq(min.rental, max.rental, by = stdev.rental), max.rental)
# "Jenk's" Intervals
jenks.intervals <- classIntervals(boundaries_with_pop$rental_density, n=7, style = 'jenks')$brks
## Warning in classIntervals(boundaries_with_pop$rental_density, n = 7, style =
## "jenks"): var has missing values, omitted in finding classes
# Assign breaks to variable
# Equal interval
boundaries_with_pop$rental.equal <- cut(boundaries_with_pop$rental_density, breaks = equal.interval, include.lowest = TRUE)
# Quantile intervals
boundaries_with_pop$rental.quantile <- cut(boundaries_with_pop$rental_density, breaks = quantile.interval, include.lowest = TRUE)
# St Dev Intervals
boundaries_with_pop$rental.sd <- cut(boundaries_with_pop$rental_density, breaks = std.interval, include.lowest = TRUE)
# Jenk's intervals
boundaries_with_pop$rental.jenks <- cut(boundaries_with_pop$rental_density, breaks = jenks.intervals, include.lowest = TRUE)
Plotting these different intervals
barcharts <- function(break_col) {
boundaries_with_pop %>%
filter(!is.na(rental_density)) %>%
ggplot(mapping = aes(x = fct_reorder(MAPLABEL, -rental_density),
y = rental_density)) +
geom_col(aes(fill = .data[[break_col]])) +
theme(axis.text.x = element_blank(),
axis.ticks.x = element_blank()) +
scale_y_continuous(name = "Proportion of Rentals") +
scale_x_discrete(name = NULL) +
scale_fill_discrete(guide = FALSE) +
ggtitle(break_col)
}
plot_grid(
barcharts("rental.equal"),
barcharts("rental.quantile"),
barcharts("rental.sd"),
barcharts("rental.jenks"),
nrow = 2,
ncol = 2
)
## Warning: The `guide` argument in `scale_*()` cannot be `FALSE`. This was deprecated in
## ggplot2 3.3.4.
## ℹ Please use "none" instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
Applying these intervals to the map.
map_intervals <- function(break_col) {
boundaries_with_pop %>%
ggplot() +
geom_sf(aes(fill = .data[[break_col]])) +
geom_sf(data = river_boundaries, fill = 'lightblue1', size = 0) +
theme_classic() +
labs(title = break_col,
subtitle = "Proportion of homes that are occupied by renters, by neighborhood",
y = "Latitude",
x = "Longitude",
fill = "Proportion of Rentals") +
scale_fill_brewer(type = 'seq',
palette = 'RdPu')
}
map_intervals("rental.equal")
map_intervals("rental.quantile")
map_intervals("rental.sd")
map_intervals("rental.jenks")