{"spec_id":"hexbin-map-geographic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nhexbin-map-geographic: Hexagonal Binning Map\nLibrary: plotnine 0.15.4 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-05-27\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    coord_fixed,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_label,\n    geom_polygon,\n    ggplot,\n    labs,\n    scale_fill_gradient,\n    theme,\n    theme_minimal,\n)\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\nWATER_COLOR = \"#C5D9EF\" if THEME == \"light\" else \"#1A2535\"\nLAND_COLOR = \"#DEDAD2\" if THEME == \"light\" else \"#2C2C27\"\nLAND_EDGE = \"#9A9A92\" if THEME == \"light\" else \"#4A4A42\"\n\n# Data — NYC taxi pickup clusters (denser hotspots for full gradient utilization)\nnp.random.seed(42)\n\ndowntown_lon = np.random.normal(-73.99, 0.012, 2500)\ndowntown_lat = np.random.normal(40.75, 0.010, 2500)\n\nmidtown_lon = np.random.normal(-73.97, 0.015, 2200)\nmidtown_lat = np.random.normal(40.78, 0.012, 2200)\n\nairport_lon = np.random.normal(-73.79, 0.010, 1500)\nairport_lat = np.random.normal(40.65, 0.008, 1500)\n\nbrooklyn_lon = np.random.normal(-73.95, 0.015, 900)\nbrooklyn_lat = np.random.normal(40.67, 0.010, 900)\n\nscatter_lon = np.random.uniform(-74.05, -73.70, 400)\nscatter_lat = np.random.uniform(40.60, 40.85, 400)\n\nlon = np.concatenate([downtown_lon, midtown_lon, airport_lon, brooklyn_lon, scatter_lon])\nlat = np.concatenate([downtown_lat, midtown_lat, airport_lat, brooklyn_lat, scatter_lat])\n\n# Optional value: simulated trip duration (min) — airport trips notably longer\nvalue = np.concatenate(\n    [\n        np.random.normal(11, 3, 2500),  # downtown — short rides\n        np.random.normal(13, 3, 2200),  # midtown\n        np.random.normal(38, 6, 1500),  # airport — long rides\n        np.random.normal(10, 3, 900),  # brooklyn\n        np.random.normal(15, 5, 400),  # scattered\n    ]\n)\n\n# Hexagonal bin polygon construction\ngridsize = 30\nLON_MIN, LON_MAX = -74.20, -73.55\nLAT_MIN, LAT_MAX = 40.52, 40.92\n\nhex_width = (LON_MAX - LON_MIN) / gridsize\nhex_radius = hex_width / np.sqrt(3)\nrow_height = hex_radius * 1.5\n\ncenters = []\nrow = 0\ny_pos = LAT_MIN\nwhile y_pos <= LAT_MAX:\n    x_offset = (hex_width / 2) if row % 2 else 0\n    x_pos = LON_MIN + x_offset\n    while x_pos <= LON_MAX:\n        centers.append((x_pos, y_pos))\n        x_pos += hex_width\n    y_pos += row_height\n    row += 1\n\npoints = np.column_stack([lon, lat])\nrecords = []\nhex_id = 0\n\nfor cx, cy in centers:\n    dx = np.abs(points[:, 0] - cx)\n    dy = np.abs(points[:, 1] - cy)\n    in_hex = (\n        (dy <= hex_radius)\n        & (dx <= hex_width / 2)\n        & (hex_radius * hex_width / 2 >= dx * hex_radius + dy * hex_width / 4)\n    )\n    count = np.sum(in_hex)\n    if count > 0:\n        mean_value = np.mean(value[in_hex])  # mean aggregation of optional value field\n        angles = np.arange(6) * np.pi / 3 + np.pi / 6\n        for angle in angles:\n            records.append(\n                {\n                    \"lon\": cx + hex_radius * np.cos(angle),\n                    \"lat\": cy + hex_radius * np.sin(angle),\n                    \"hex_id\": hex_id,\n                    \"count\": count,\n                    \"mean_value\": mean_value,\n                }\n            )\n        hex_id += 1\n\nhex_df = pd.DataFrame(records)\n\n# Hotspot label positions for direct annotation\nlabels_df = pd.DataFrame(\n    {\"lon\": [-73.99, -73.97, -73.79], \"lat\": [40.762, 40.793, 40.661], \"label\": [\"Downtown\", \"Midtown\", \"JFK Airport\"]}\n)\n\n# NYC-like coastline boundary\ncoast_lon = [\n    -74.20,\n    -74.12,\n    -74.05,\n    -74.00,\n    -73.95,\n    -73.88,\n    -73.80,\n    -73.72,\n    -73.62,\n    -73.55,\n    -73.55,\n    -73.60,\n    -73.68,\n    -73.75,\n    -73.82,\n    -73.88,\n    -73.94,\n    -74.00,\n    -74.06,\n    -74.12,\n    -74.18,\n    -74.20,\n]\ncoast_lat = [\n    40.52,\n    40.50,\n    40.51,\n    40.54,\n    40.57,\n    40.61,\n    40.66,\n    40.70,\n    40.76,\n    40.81,\n    40.92,\n    40.93,\n    40.92,\n    40.90,\n    40.88,\n    40.85,\n    40.81,\n    40.76,\n    40.68,\n    40.61,\n    40.56,\n    40.52,\n]\ndf_coastline = pd.DataFrame({\"region\": \"land\", \"order\": range(len(coast_lon)), \"lon\": coast_lon, \"lat\": coast_lat})\n\ntitle = \"hexbin-map-geographic · python · plotnine · anyplot.ai\"\n\n# sqrt transform spreads color across the right-skewed count distribution —\n# a ggplot2 grammar-of-graphics feature that replaces manual data normalization\nplot = (\n    ggplot()\n    + geom_polygon(\n        aes(x=\"lon\", y=\"lat\", group=\"region\"), data=df_coastline, fill=LAND_COLOR, color=LAND_EDGE, size=0.5, alpha=0.9\n    )\n    + geom_polygon(\n        aes(x=\"lon\", y=\"lat\", group=\"hex_id\", fill=\"count\"), data=hex_df, color=PAGE_BG, size=0.15, alpha=0.88\n    )\n    + geom_label(\n        aes(x=\"lon\", y=\"lat\", label=\"label\"),\n        data=labels_df,\n        color=INK,\n        fill=ELEVATED_BG,\n        size=3,\n        label_padding=0.2,\n        label_size=0.3,\n    )\n    + scale_fill_gradient(low=\"#009E73\", high=\"#4467A3\", name=\"Pickups\", trans=\"sqrt\")\n    + coord_fixed(ratio=1.0, xlim=(-74.20, -73.55), ylim=(40.52, 40.92))\n    + labs(title=title, x=\"Longitude (°)\", y=\"Latitude (°)\")\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=WATER_COLOR),\n        panel_border=element_rect(color=INK_SOFT, fill=None),\n        panel_grid_major=element_blank(),\n        panel_grid_minor=element_blank(),\n        plot_title=element_text(size=12, color=INK),\n        axis_title=element_text(size=10, color=INK),\n        axis_text=element_text(size=8, color=INK_SOFT),\n        legend_title=element_text(size=8, color=INK),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_position=\"right\",\n        axis_line=element_line(color=INK_SOFT, size=0.3),\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\")\n"}