Visualizing and Maintaining the Green Canopy of NYC
Author
Sojung Chu
Published
November 14, 2025
NYC Parks logo
Executive Summary
This report explores the NYC TreeMap data set to create visualizations of NYC’s urban fabric. Based on these visualizations, the city council proposes a new program for the NYC Parks Department that aims to make the benefit of NYC trees available to all New Yorkers.
Methodology
The following methods were used to obtain and prepare the data for analysis.
Data Acquisition
NYC District boundaries data was responsibly acquired from the NYC Department of Planning since NYC is divided into 51 City Council Districts.
Code
library(sf)library(tidyverse)library(dplyr)library(ggplot2)get_nyc_council <-function(url ="https://s-media.nyc.gov/agencies/dcp/assets/files/zip/data-tools/bytes/city-council/nycc_25c.zip",data_dir ="data/mp03") {# i. Create data/mp03 directory only if neededif (!dir.exists(data_dir)) {dir.create(data_dir, recursive =TRUE) }# Path to the local zip file zip_path <-file.path(data_dir, basename(url))# ii. Download the zip file only if neededif (!file.exists(zip_path)) {download.file(url, destfile = zip_path, mode ="wb") }# iii. Unzip only if needed (i.e., if no shapefile is present yet) shp_files <-list.files( data_dir,pattern ="\\.shp$",full.names =TRUE,recursive =TRUE )if (length(shp_files) ==0) {unzip(zip_path, exdir = data_dir) shp_files <-list.files( data_dir,pattern ="\\.shp$",full.names =TRUE,recursive =TRUE ) }if (length(shp_files) ==0) {stop("No shapefile found in the unzipped City Council data.") }# iv. Read the shapefile with sf::st_read council_raw <-st_read(shp_files[1], quiet =TRUE)# v. Transform to WGS 84 council_wgs84 <-st_transform(council_raw, crs ="WGS84")# vi. Return transformed data council_wgs84}nyc_council <-get_nyc_council()
NYC Forestry Tree Points data was acquired from the NYC OpenData through an API.
To be responsible, this code pages through the entire data set, with a suitable use of $limit and $offset parameters in the API call to ensure the entire data set is downloaded. It saves the result of each query to a separate file in data/mp03 with a consistent naming schema. It also only download files if they are not already saved.
Code
if (!dir.exists("data/mp03")) {dir.create("data/mp03", showWarnings =FALSE, recursive =TRUE)}get_nyc_tree_points <-function() {# Packagesif (!require("httr2")) install.packages("httr2")library(httr2)if (!require("sf")) install.packages("sf")library(sf)if (!require("dplyr")) install.packages("dplyr")library(dplyr)# Endpoint & Paging Setup ENDPOINT <-"https://data.cityofnewyork.us/resource/hn5i-inap.geojson" BATCH_SIZE <-50000# $limit OFFSET <-0# $offset END_OF_EXPORT <-FALSE FILE_INDEX <-1 ALL_DATA <-list()while (!END_OF_EXPORT) { file_name <-sprintf("nyc_treepoints_%04d.geojson", FILE_INDEX) file_path <-file.path("data/mp03", file_name)# i. Only download if not already saved locallyif (!file.exists(file_path)) { req <-request(ENDPOINT) |>req_url_query(`$limit`= BATCH_SIZE,`$offset`= OFFSET ) resp <-req_perform(req)resp_body_raw(resp) |>writeBin(con = file_path) }# ii. Read this page with st_read batch_data <-st_read(file_path, quiet =TRUE) ALL_DATA <-c(ALL_DATA, list(batch_data))# Check for end of datasetif (NROW(batch_data) != BATCH_SIZE) { END_OF_EXPORT <-TRUE } else { OFFSET <- OFFSET + BATCH_SIZE FILE_INDEX <- FILE_INDEX +1 } }# iii. Combine all pages with bind_rows ALL_DATA <-bind_rows(ALL_DATA)return(ALL_DATA)}trees <-get_nyc_tree_points()
Data Integration
The NYC trees were first mapped by creating a ggplot2 map that superimposes trees as points over the council districts of NYC. Two layers were used to have different spatial elements, one with the council district boundaries and another with points representing each tree.
The district boundary lines were also simplified beforehand for plotting purposes because NYC District shape files are very high resolution and can be somewhat slow to plot.
Code
# Simplificationnyc_council_simplified <- nyc_council |>mutate(geometry =st_simplify(geometry, dTolerance =20) )ggplot() +# Council district boundaries layer (POLYGON)geom_sf(data = nyc_council_simplified,fill =NA,color ="black",linewidth =0.3 ) +# Tree points layer (POINT)geom_sf(data = trees,color ="darkgreen",alpha =0.02,size =0.0005 ) +coord_sf() +theme_minimal() +labs(title ="Figure 1: NYC Trees",caption ="Source: NYC Open Data & NYC Dept. of Planning" )
Improved Tree Map Visualization
The map of all trees is a bit too dense to be truly legible. The following is an interactive visualization to improve legibility.
Code
if (!require("leaflet")) install.packages("leaflet")library(leaflet)set.seed(123)# Sample trees just for performancetrees_sample <- trees |>slice_sample(n =20000) leaflet(options =leafletOptions(preferCanvas =TRUE)) |>addProviderTiles("CartoDB.Positron") |># Title (centered)addControl(html ="<h3 style='text-align:center; margin:4px;'>Figure 2: Interactive Map for NYC Tree Locations</h3>",position ="topleft") |># Council district boundariesaddPolygons(data = nyc_council_simplified,color ="black",weight =1,fillColor ="grey92",fillOpacity =0.4 ) |># Tree locations as simple pointsaddCircleMarkers(data = trees_sample,radius =2,color ="#006400", # dark greenstroke =FALSE,fillOpacity =0.5 )
Overview
Exploratory data analysis (EDA) was conducted using the st_join function which implements spatial joins between the Tree Points data and the District boundaries. These were the key findings:
Code
# Spatial join between the datasetstrees_with_district <-st_join( trees, nyc_council,join = st_intersects)# Count trees by districttrees_by_district <- trees_with_district |>st_drop_geometry() |>group_by(CounDist) |>summarise(n_trees =n()) |>arrange(desc(n_trees))# Which council district has the most trees?district_most_trees <- trees_by_district |>slice_max(n_trees, n =1)invisible(district_most_trees)
Council District 51 has the most trees.
Code
# Summarise trees and area per council districttrees_density <- trees_with_district |>st_drop_geometry() |>group_by(CounDist, Shape_Area) |>summarise(n_trees =n()) |>#ungroup() |>mutate(tree_density = n_trees / Shape_Area ) |>arrange(desc(tree_density))# District with the highest tree densitydistrict_highest_density <- trees_density |>slice_max(tree_density, n =1)invisible(district_highest_density)
Council District 7 has the highest density of trees.
Code
# Count of total trees and dead trees per districtdead_fraction <- trees_with_district |>st_drop_geometry() |>group_by(CounDist) |>summarise(total_trees =n(),dead_trees =sum(tpcondition =="Dead", na.rm =TRUE) ) |>ungroup() |>mutate(fraction_dead = dead_trees / total_trees ) |>arrange(desc(fraction_dead))# District with the highest fraction of dead treesdistrict_highest_dead_fraction <- dead_fraction |>slice_max(fraction_dead, n =1)invisible(district_highest_dead_fraction)
Council District 32 has the highest fraction of dead trees.
Code
# Add Borough based on council district numbertrees_with_borough <- trees_with_district |>mutate(Borough =case_when( CounDist >=1& CounDist <=10~"Manhattan", CounDist >=11& CounDist <=18~"Bronx", CounDist >=19& CounDist <=32~"Queens", CounDist >=33& CounDist <=48~"Brooklyn", CounDist >=49& CounDist <=51~"Staten Island", ) )# Keep only trees in Manhattanmanhattan_trees <- trees_with_borough |>filter(Borough =="Manhattan")# Count species in Manhattanmost_common_manhattan_species <- manhattan_trees |>st_drop_geometry() |>count(genusspecies, sort =TRUE) |>slice(1)invisible(most_common_manhattan_species)
The most common tree species in Manhattan is Gleditsia triacanthos var. inermis - Thornless honeylocust.
Code
# Helper to create a point with WGS84 CRSnew_st_point <-function(lat, lon, ...) {# st_sfc expects x = lon, y = latst_sfc(point =st_point(c(lon, lat))) |>st_set_crs("WGS84")}# Create a point for Baruch's campusmy_point <-new_st_point(lat =40.7402,lon =-73.9834)# 2. Compute distance from each tree to Baruch's campustrees_with_distance <- trees |>mutate(distance =st_distance(geometry, my_point) )# 3. Find the single closest treeclosest_tree <- trees_with_distance |>slice_min(distance, n =1)closest_tree_species <- closest_tree |>st_drop_geometry() |>pull(genusspecies)invisible(closest_tree_species)
The tree closest to Baruch’s campus is a Gleditsia triacanthos - Honeylocust.
Proposed NYC Parks Tree Program
The following proposal details a new tree initiative for Council District 26, serving the neighborhoods of Long Island City, Sunnyside, Astoria, and Woodside.
Shade Equity: Increasing Tree Canopy for District 26
Description
As a resident and council member of District 26, I am proposing a targeted “Shade Equity in District 26” program to increase the amount of tree canopy. District 26 is one of four high-foot-traffic waterfront districts (1, 26, 33, 38). Within this group, it ranks last in shade-producing trees in ratio to its area. District 26 has seen high residential development and heavy pedestrian use, but much of the area remains exposed to full sun. This increases localized heat buildup and makes the district less comfortable for residents to enjoy during hotter seasons.
Scope
Code
# Define a set of broad-canopy "shade" species by species name.shade_species <-c(# Maples"Acer nigrum - black maple","Acer platanoides - Norway maple","Acer saccharinum - silver maple","Acer rubrum - red maple","Acer saccharum - sugar maple",# Plane / sycamore"Platanus x acerifolia - London planetree","Platanus occidentalis - American sycamore","Platanus x acerifolia 'Bloodgood' - 'Bloodgood' London planetree","Platanus x acerfolia 'Exclamation' - 'Exclamation' London planetree","Platanus x acerifolia 'Columbia' - 'Columbia' London planetree",# Lindens (Tilia)"Tilia cordata - littleleaf linden","Tilia x. europaea - Common linden","Tilia americana - American basswood","Tilia tomentosa - silver linden","Tilia platyphyllos - largeleaf linden","Tilia americana 'Redmond' - 'Redmond' American linden","Tilia americana 'McSentry' - 'McSentry' American linden",# Honeylocust"Gleditsia triacanthos var. inermis - Thornless honeylocust","Gleditsia triacanthos - Honeylocust","Gleditsia triacanthos var. inermis 'Streetkeeper' - 'Streetkeeper' Thornless honeylocust","Gleditsia triacanthos var. inermis 'Skyline' - 'Skyline' Thornless honeylocust","Gleditsia triacanthos var. inermis 'Imperial' - 'Imperial' Thornless honeylocust","Gleditsia triacanthos var. inermis 'Shademaster' - 'Shademaster' Thornless honeylocust","Gleditsia triacanthos var. inermis 'Northern Acclaim' - 'Northern Acclaim' Thornless honeylocust",# Oaks (Quercus)"Quercus palustris - pin oak","Quercus phellos - willow oak","Quercus rubra - northern red oak","Quercus alba - white oak","Quercus bicolor - swamp white oak","Quercus x sargentii - Sargent oak","Quercus velutina - black oak","Quercus robur - English oak","Quercus acutissima - sawtooth oak","Quercus macrocarpa - bur oak","Quercus muehlenbergii - chinkapin oak","Quercus lyrata - overcup oak","Quercus coccinea - scarlet oak","Quercus stellata - post oak","Quercus virginiana - live oak","Quercus cerris - European turkey oak","Quercus shumardii - Shumard's oak","Quercus texana - Nuttall oak","Quercus nuttallii - Nuttall oak","Quercus nigra - water oak",# Elms"Ulmus americana - American elm","Ulmus parvifolia - Chinese elm","Ulmus 'Patriot' - 'Patriot' Elm","Ulmus 'Frontier' - 'Frontier' Elm","Ulmus americana 'Valley Forge' - 'Valley Forge' American Elm","Ulmus americana 'Princeton' - 'Princeton' American elm","Ulmus propinqua 'Emerald Sunshine' - 'Emerald Sunshine' Elm","Ulmus 'New Harmony' - 'New Harmony' Elm",# Sweetgum, tuliptree, coffeetree, beech, hackberry, blackgum"Liquidambar styraciflua - sweetgum","Liquidambar styraciflua 'Moraine' - 'Moraine' Sweetgum","Liquidambar styraciflua 'Cherokee' - 'Cherokee' Sweetgum","Liquidambar styraciflua 'Grandmaster' - 'Grandmaster' Sweetgum","Liriodendron tulipifera - tuliptree","Liriodendron tulipifera 'Arnold' - 'Arnold' Tulip poplar","Liriodendron tulipifera 'Emerald City' - 'Emerald City' Tulip poplar","Gymnocladus dioicus - Kentucky coffeetree","Gymnocladus dioicus 'Espresso' - 'Espresso' Kentucky coffee tree","Gymnocladus dioicus 'Prairie Titan' - 'Prairie Titan' Kentucky coffee tree","Fagus grandifolia - American beech","Fagus sylvatica - European beech","Celtis occidentalis - common hackberry","Celtis laevigata - sugarberry","Nyssa sylvatica - blackgum","Nyssa sylvatica 'Tupelo Tower' - 'Tupelo Tower' Black gum","Nyssa sylvatica 'Red Rage' - 'Red Rage' Black gum","Nyssa sylvatica 'Wildfire' - 'Wildfire' Black gum")# Filter to District 26 and flag shade treesdistrict26_trees <- trees_with_borough |>filter(CounDist ==26)district26_shade_summary <- district26_trees |>mutate(is_shade = genusspecies %in% shade_species ) |>summarise(total_trees =n(),shade_trees =sum(is_shade, na.rm =TRUE) ) |>mutate(shade_target_20pct =round(shade_trees *0.20) )invisible(district26_shade_summary)
Prior to analysis, a set of broad-canopy “shade” tree species (including maples, plane trees, lindens, oaks, elms, and other large-canopy species) was defined and used to identify shade trees.
Code
# Visualization of shade trees in District 26district26_shade_trees <- district26_trees |>mutate(is_shade = genusspecies %in% shade_species ) |>filter(is_shade)# District 26 area onlydistrict26_boundary <- nyc_council_simplified |>filter(CounDist ==26)# Plot the treesggplot() +# District 26 boundary (polygon)geom_sf(data = district26_boundary,fill =NA,color ="black",linewidth =0.4 ) +# Shade trees in District 26 (points)geom_sf(data = district26_shade_trees,color ="darkgreen",alpha =0.5,size =0.5 ) +coord_sf() +theme_minimal() +labs(title ="Figure 3: Shade Trees in NYC Council District 26",subtitle ="Filtered for large-canopy species",caption ="Source: NYC Open Data & NYC Dept. of Planning" )
District 26 currently has 8412 shade trees. Add 20% more, or 1682 new shade trees.
Replace the 432 stumps in District 26 with new plantings.
The altogether addition of 2114 new trees (additional shade trees + replacement of stumps) would increase the total tree count of 15380 trees by ~14%.
Prioritize broad-canopy species (London plane, red maple, or other large-crown species) to maximize shade per planting location.
Why District 26?
District 26 was compared to three other similarly dense districts with significant waterfront access and heavy foot traffic: District 1 (Lower Manhattan), District 33 (DUMBO/Brooklyn Heights/Williamsburg/Greenpoint), and District 38 (Red Hook/Sunset Park).
Code
# Comparison of shade trees between district 1, 26, 33, 38shade_by_district <- trees_with_borough |>mutate(is_shade = genusspecies %in% shade_species ) |>st_drop_geometry() |>group_by(CounDist) |>summarise(total_trees =n(),shade_trees =sum(is_shade, na.rm =TRUE) ) |>ungroup()# Area for each district from nyc_councildistrict_area <- nyc_council |>st_drop_geometry() |>select(CounDist, Shape_Area)# Join counts with area and compute shade-tree densityshade_density <- shade_by_district |>left_join(district_area, by ="CounDist") |>mutate(shade_trees_per_million = shade_trees / Shape_Area *1e6 )# Filter districtsshade_density_focus <- shade_density |>filter(CounDist %in%c(1, 26, 33, 38)) |>arrange(CounDist)# Bar chart comparing shade-tree densityggplot(shade_density_focus, aes(x =factor(CounDist),y = shade_trees_per_million)) +geom_col(fill ="darkgreen", alpha =0.7) +theme_minimal() +labs(title =" Figure 4: District Shade Trees",x ="Council District",y ="Shade Trees per Million Sq Feet" )
District 26 has the lowest density of shade trees among the comparison districts, only about 50 shade trees per million square feet, which is below Districts 1, 33, and 38.
The following visualization illustrates the contrast in shade tree coverage between Districts 26 and 33, neighboring areas that differ significantly in their shade-tree-to-area ratios.
Code
# Trees in District 33district33_trees <- trees_with_borough |>filter(CounDist ==33)# Shade trees in District 33district33_shade_trees <- district33_trees |>mutate(is_shade = genusspecies %in% shade_species ) |>filter(is_shade)# District 33 area onlydistrict33_boundary <- nyc_council_simplified |>filter(CounDist ==33)library(patchwork)# District 26 shade mapp26 <-ggplot() +geom_sf(data = district26_boundary,fill =NA,color ="black",linewidth =0.4 ) +geom_sf(data = district26_shade_trees,color ="darkgreen",alpha =0.5,size =0.5 ) +coord_sf() +theme_minimal() +theme(axis.text =element_blank(),axis.ticks =element_blank(),axis.title =element_blank() ) +labs(title ="Figure 5: Shade Trees in District 26", )# District 33 shade mapp33 <-ggplot() +geom_sf(data = district33_boundary,fill =NA,color ="black",linewidth =0.4 ) +geom_sf(data = district33_shade_trees,color ="darkgreen",alpha =0.5,size =0.5 ) +coord_sf() +theme_minimal() +theme(axis.text =element_blank(),axis.ticks =element_blank(),axis.title =element_blank() ) +labs(title ="Figure 6: Shade Trees in District 33", )p26 + p33 +plot_layout(widths =c(1, 1))
District 26 also has the highest ratio of stumps to total tree sites. In direct comparison, it has significantly higher ratio of stumps to total tree sites than District 1 and slightly higher than District 33 and 38, indicating unrealized planting opportunities.
Code
# Computes stump ratio per districtstumps_by_district <- trees_with_borough |>st_drop_geometry() |>group_by(CounDist) |>summarise(total_sites =n(),n_stumps =sum(tpstructure =="Stump", na.rm =TRUE) ) |>ungroup() |>mutate(stump_fraction = n_stumps / total_sites )# Comparison of districtsstumps_focus <- stumps_by_district |>filter(CounDist %in%c(1, 26, 33, 38)) |>arrange(CounDist)# Bar chartggplot( stumps_focus,aes(x =factor(CounDist),y = stump_fraction,fill = CounDist ==26 )) +# highlight District 26geom_col(alpha =0.8) +scale_fill_manual(values =c("TRUE"="darkgreen", "FALSE"="grey70"),guide ="none" ) +scale_y_continuous(labels = scales::percent_format(accuracy =1)) +theme_minimal() +labs(title ="Figure 7: Stumps as a Share of All Tree Sites",x ="Council District",y ="Stumps / Total Sites" )
Conclusion
In conclusion, District 26 faces a clear shade equity gap compared with other highly populated waterfront districts. Despite significant residential growth, it has the lowest density of shade-producing trees, a higher stump-to-tree ratio, and large stretches of fully exposed pedestrian areas.