|
| 1 | +#' anyplot.ai |
| 2 | +#' hexbin-map-geographic: Hexagonal Binning Map |
| 3 | +#' Library: ggplot2 3.5.1 | R 4.4.1 |
| 4 | +#' Quality: 89/100 | Created: 2026-05-27 |
| 5 | + |
| 6 | +library(ggplot2) |
| 7 | +library(dplyr) |
| 8 | +library(tibble) |
| 9 | +library(scales) |
| 10 | +library(ragg) |
| 11 | + |
| 12 | +set.seed(42) |
| 13 | + |
| 14 | +# Theme tokens |
| 15 | +THEME <- Sys.getenv("ANYPLOT_THEME", "light") |
| 16 | +PAGE_BG <- if (THEME == "light") "#FAF8F1" else "#1A1A17" |
| 17 | +ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420" |
| 18 | +INK <- if (THEME == "light") "#1A1A17" else "#F0EFE8" |
| 19 | +INK_SOFT <- if (THEME == "light") "#4A4A44" else "#B8B7B0" |
| 20 | +MAP_WATER <- if (THEME == "light") "#C8DCE8" else "#1A2530" |
| 21 | +MAP_LAND <- if (THEME == "light") "#E6E0D4" else "#2C2A22" |
| 22 | +MAP_BORDER <- if (THEME == "light") "#9A9890" else "#58584E" |
| 23 | + |
| 24 | +# NYC metro area taxi pickups — simulated density clusters |
| 25 | +# Centres: Midtown, Greenwich Village, UWS, Chelsea, S.Brooklyn, |
| 26 | +# Midtown East, Lower Manhattan, JFK-adjacent, LGA-adjacent, Jersey City |
| 27 | +cluster_centers <- tibble::tibble( |
| 28 | + lat = c(40.756, 40.730, 40.775, 40.744, 40.676, |
| 29 | + 40.762, 40.710, 40.645, 40.769, 40.727), |
| 30 | + lon = c(-73.989, -74.003, -73.981, -73.995, -73.982, |
| 31 | + -73.971, -73.997, -73.794, -73.863, -74.077), |
| 32 | + n = c(5000L, 3200L, 2400L, 2600L, 1800L, |
| 33 | + 1400L, 1000L, 700L, 600L, 500L), |
| 34 | + s_lat = c(0.010, 0.009, 0.011, 0.009, 0.013, |
| 35 | + 0.010, 0.009, 0.009, 0.008, 0.009), |
| 36 | + s_lon = c(0.013, 0.011, 0.013, 0.011, 0.015, |
| 37 | + 0.012, 0.011, 0.012, 0.010, 0.011) |
| 38 | +) |
| 39 | + |
| 40 | +pickups <- dplyr::bind_rows( |
| 41 | + lapply(seq_len(nrow(cluster_centers)), function(i) { |
| 42 | + cc <- cluster_centers[i, ] |
| 43 | + tibble::tibble( |
| 44 | + lat = rnorm(cc$n, cc$lat, cc$s_lat), |
| 45 | + lon = rnorm(cc$n, cc$lon, cc$s_lon) |
| 46 | + ) |
| 47 | + }) |
| 48 | +) |> dplyr::filter( |
| 49 | + lat >= 40.60, lat <= 40.83, |
| 50 | + lon >= -74.15, lon <= -73.75 |
| 51 | +) |
| 52 | + |
| 53 | +# --- Simplified NYC geographic context (hand-digitized approximate outlines) --- |
| 54 | +# sf/rnaturalearth not available; inline polygons provide coastline context. |
| 55 | +# Water shows as panel background (MAP_WATER). |
| 56 | + |
| 57 | +# New Jersey — land mass west of the Hudson River |
| 58 | +nj_land <- tibble::tibble( |
| 59 | + group = "nj", |
| 60 | + lon = c(-74.15, -74.15, -73.960, -73.980, -74.015, -74.034, -74.050, |
| 61 | + -74.085, -74.115, -74.15), |
| 62 | + lat = c(40.60, 40.83, 40.830, 40.800, 40.765, 40.725, 40.700, |
| 63 | + 40.660, 40.630, 40.60) |
| 64 | +) |
| 65 | + |
| 66 | +# Manhattan Island — narrow north-south strip between Hudson and East River. |
| 67 | +# Points trace the eastern shoreline (Battery → Harlem) then west (Harlem → Battery). |
| 68 | +manhattan_land <- tibble::tibble( |
| 69 | + group = "manhattan", |
| 70 | + lon = c( |
| 71 | + # East shore: south to north |
| 72 | + -73.971, -73.972, -73.975, -73.971, -73.965, -73.952, -73.940, -73.928, |
| 73 | + # North clip at lat 40.830 (Inwood not in view) |
| 74 | + -73.934, -73.943, |
| 75 | + # West shore: north to south |
| 76 | + -73.955, -73.965, -73.973, -73.982, -73.991, -74.001, -74.010, -74.014, -73.971 |
| 77 | + ), |
| 78 | + lat = c( |
| 79 | + 40.700, 40.725, 40.742, 40.758, 40.764, 40.788, 40.808, 40.830, |
| 80 | + 40.830, 40.822, |
| 81 | + 40.813, 40.800, 40.782, 40.764, 40.749, 40.732, 40.715, 40.703, 40.700 |
| 82 | + ) |
| 83 | +) |
| 84 | + |
| 85 | +# Brooklyn and Queens — southeastern land mass south of the East River |
| 86 | +bq_land <- tibble::tibble( |
| 87 | + group = "bq", |
| 88 | + lon = c(-74.030, -73.993, -73.975, -73.960, -73.938, -73.880, -73.800, |
| 89 | + -73.75, -73.75, -74.030), |
| 90 | + lat = c(40.700, 40.698, 40.684, 40.672, 40.665, 40.651, 40.636, |
| 91 | + 40.628, 40.60, 40.60) |
| 92 | +) |
| 93 | + |
| 94 | +nyc_geo <- dplyr::bind_rows(nj_land, manhattan_land, bq_land) |
| 95 | + |
| 96 | +# --- Plot --- |
| 97 | + |
| 98 | +plot_title <- "hexbin-map-geographic · r · ggplot2 · anyplot.ai" |
| 99 | +title_size <- max(8L, round(12 * min(1.0, 67 / nchar(plot_title)))) |
| 100 | + |
| 101 | +p <- ggplot() + |
| 102 | + # Geographic base map |
| 103 | + geom_polygon( |
| 104 | + data = nyc_geo, |
| 105 | + aes(x = lon, y = lat, group = group), |
| 106 | + fill = MAP_LAND, |
| 107 | + color = MAP_BORDER, |
| 108 | + linewidth = 0.30 |
| 109 | + ) + |
| 110 | + # Hexbin density layer |
| 111 | + geom_hex( |
| 112 | + data = pickups, |
| 113 | + aes(x = lon, y = lat), |
| 114 | + bins = 38, |
| 115 | + alpha = 0.88 |
| 116 | + ) + |
| 117 | + # Storytelling annotation: point to primary hotspot |
| 118 | + annotate("segment", |
| 119 | + x = -73.975, xend = -73.989, |
| 120 | + y = 40.773, yend = 40.762, |
| 121 | + color = INK, linewidth = 0.45, |
| 122 | + arrow = arrow(length = unit(0.18, "cm"), type = "closed") |
| 123 | + ) + |
| 124 | + annotate("text", |
| 125 | + x = -73.968, y = 40.776, |
| 126 | + label = "Midtown hotspot", |
| 127 | + color = INK, |
| 128 | + size = 2.7, |
| 129 | + fontface = "italic", |
| 130 | + hjust = 0 |
| 131 | + ) + |
| 132 | + scale_fill_gradient( |
| 133 | + low = "#009E73", |
| 134 | + high = "#4467A3", |
| 135 | + name = "Pickup\nCount", |
| 136 | + labels = scales::comma, |
| 137 | + guide = guide_colorbar(barwidth = 0.8, barheight = 8) |
| 138 | + ) + |
| 139 | + scale_x_continuous( |
| 140 | + labels = function(x) sprintf("%.2f°W", -x), |
| 141 | + expand = expansion(mult = 0.0) |
| 142 | + ) + |
| 143 | + scale_y_continuous( |
| 144 | + labels = function(y) sprintf("%.2f°N", y), |
| 145 | + expand = expansion(mult = 0.0) |
| 146 | + ) + |
| 147 | + coord_fixed( |
| 148 | + ratio = 1, |
| 149 | + xlim = c(-74.15, -73.75), |
| 150 | + ylim = c(40.60, 40.83) |
| 151 | + ) + |
| 152 | + labs( |
| 153 | + x = "Longitude", |
| 154 | + y = "Latitude", |
| 155 | + title = plot_title |
| 156 | + ) + |
| 157 | + theme_minimal(base_size = 8) + |
| 158 | + theme( |
| 159 | + plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG), |
| 160 | + panel.background = element_rect(fill = MAP_WATER, color = NA), |
| 161 | + panel.grid.major = element_line(color = INK_SOFT, linewidth = 0.10), |
| 162 | + panel.grid.minor = element_blank(), |
| 163 | + axis.title = element_text(color = INK, size = 10), |
| 164 | + axis.text = element_text(color = INK_SOFT, size = 8), |
| 165 | + plot.title = element_text(color = INK, size = title_size, face = "bold"), |
| 166 | + legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT, linewidth = 0.3), |
| 167 | + legend.text = element_text(color = INK_SOFT, size = 8), |
| 168 | + legend.title = element_text(color = INK, size = 9), |
| 169 | + legend.position = "right", |
| 170 | + plot.margin = margin(0.4, 0.4, 0.4, 0.4, "cm") |
| 171 | + ) |
| 172 | + |
| 173 | +ggsave( |
| 174 | + filename = sprintf("plot-%s.png", THEME), |
| 175 | + plot = p, |
| 176 | + device = ragg::agg_png, |
| 177 | + width = 8, |
| 178 | + height = 4.5, |
| 179 | + units = "in", |
| 180 | + dpi = 400 |
| 181 | +) |
0 commit comments