{"spec_id":"hexbin-map-geographic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nhexbin-map-geographic: Hexagonal Binning Map\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 91/100 | Created: 2026-05-27\n\"\"\"\n\nimport os\nimport sys\nfrom collections import Counter\n\n\n# Work around filename shadowing the altair library\nsys.path.pop(0)\n\nimport altair as alt\nimport numpy as np\nfrom PIL import Image\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\"\nLAND_FILL = \"#E8E4DC\" if THEME == \"light\" else \"#2C2C28\"\nLAND_STROKE = \"#B0AFA8\" if THEME == \"light\" else \"#4A4A44\"\n\n# Data: bird species sightings — deliberately wide density spread to showcase log-scale range\nnp.random.seed(42)\n# (lat, lon, n_obs, spread_deg) — from sparse (150) to heavy (2500)\nclusters = [\n    (47.6, -122.3, 2000, 0.45),  # Seattle / Pacific flyway (dense hub)\n    (37.8, -122.4, 800, 0.40),  # San Francisco Bay (moderate)\n    (29.8, -95.4, 350, 0.40),  # Houston / Gulf Coast (lighter)\n    (41.9, -87.6, 1500, 0.45),  # Chicago / Great Lakes (dense hub)\n    (40.7, -74.0, 2500, 0.40),  # New York metro (densest hub)\n    (38.9, -77.0, 500, 0.38),  # Washington DC (moderate)\n    (25.8, -80.2, 150, 0.38),  # Miami / Atlantic flyway (sparse)\n    (44.9, -93.2, 250, 0.42),  # Minneapolis / Mississippi corridor (sparse)\n]\n\nlats_all, lons_all = [], []\nfor clat, clon, n, spread in clusters:\n    lats_all.extend(np.random.normal(clat, spread, n))\n    lons_all.extend(np.random.normal(clon, spread, n))\n\nlats = np.clip(np.array(lats_all), 24.0, 50.0)\nlons = np.clip(np.array(lons_all), -126.0, -65.0)\n\n# Flat-top hexagonal binning in lat/lon space\nHEX_R = 0.65  # degrees (~65 km at mid-latitudes)\nCOL_STEP = 1.5 * HEX_R\nROW_STEP = HEX_R * np.sqrt(3)\n\ncols = np.round(lons / COL_STEP).astype(int)\nrow_off = (np.abs(cols) % 2) * 0.5\nrows = np.round(lats / ROW_STEP - row_off).astype(int)\nhex_counts = Counter(zip(cols.tolist(), rows.tolist(), strict=False))\n\nfeatures = []\nfor (c, r), count in hex_counts.items():\n    lon_c = c * COL_STEP\n    lat_c = (r + (abs(c) % 2) * 0.5) * ROW_STEP\n    if not (23.5 <= lat_c <= 51.0 and -127.0 <= lon_c <= -64.0):\n        continue\n    # Clockwise winding in geographic coords → renders as filled interior\n    # (vl-convert flips y-axis; CW geo = CCW screen = D3 interior fill)\n    verts = [\n        [lon_c + HEX_R * np.cos(np.radians(-60 * i)), lat_c + HEX_R * np.sin(np.radians(-60 * i))] for i in range(6)\n    ]\n    verts.append(verts[0])\n    features.append(\n        {\n            \"type\": \"Feature\",\n            \"geometry\": {\"type\": \"Polygon\", \"coordinates\": [verts]},\n            \"properties\": {\"count\": int(count)},\n        }\n    )\n\n# Labels for the three strongest migration hubs — guide viewer to the story\nann_data = [\n    {\"lon\": -74.0, \"lat\": 41.2, \"label\": \"NYC Metro\"},\n    {\"lon\": -122.3, \"lat\": 47.0, \"label\": \"Seattle\"},\n    {\"lon\": -87.6, \"lat\": 42.5, \"label\": \"Chicago\"},\n]\n\n# Base map — vega CDN, no local package required\nWORLD_URL = \"https://cdn.jsdelivr.net/npm/vega-datasets@v1.29.0/data/world-110m.json\"\n# Mercator projection centred on continental US, tuned to fit inner 620×320 view\nproj_params = {\"type\": \"mercator\", \"scale\": 680, \"center\": [-96, 37]}\n\nbasemap = (\n    alt.Chart(alt.topo_feature(WORLD_URL, \"land\"))\n    .mark_geoshape(fill=LAND_FILL, stroke=LAND_STROKE, strokeWidth=0.5)\n    .project(**proj_params)\n)\n\n# Hexbin layer — inline GeoJSON features, sequential colormap brand-green → blue\nhexbin = (\n    alt.Chart(alt.InlineData(values=features, format=alt.DataFormat(type=\"json\")))\n    .mark_geoshape(stroke=PAGE_BG, strokeWidth=0.3, opacity=0.85)\n    .encode(\n        color=alt.Color(\n            \"properties.count:Q\",\n            scale=alt.Scale(type=\"log\", range=[\"#009E73\", \"#4467A3\"]),\n            legend=alt.Legend(\n                title=\"Sightings\", gradientLength=140, gradientThickness=14, labelFontSize=10, titleFontSize=10\n            ),\n        ),\n        tooltip=[alt.Tooltip(\"properties.count:Q\", title=\"Sightings\")],\n    )\n    .project(**proj_params)\n)\n\n# Text annotations directing the viewer to the densest migration hubs\nannotation = (\n    alt.Chart(alt.InlineData(values=ann_data))\n    .mark_text(color=INK, fontSize=8, fontWeight=\"bold\", align=\"center\", dy=-4)\n    .encode(longitude=\"lon:Q\", latitude=\"lat:Q\", text=\"label:N\")\n    .project(**proj_params)\n)\n\nchart = (\n    alt.layer(basemap, hexbin, annotation)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.TitleParams(\n            text=\"US Bird Migration Hotspots\", subtitle=\"hexbin-map-geographic · python · altair · anyplot.ai\"\n        ),\n        padding={\"left\": 0, \"right\": 0, \"top\": 0, \"bottom\": 0},\n    )\n    .configure_title(color=INK, fontSize=18, fontWeight=\"bold\", subtitleColor=INK_SOFT, subtitleFontSize=10)\n    .configure_view(fill=PAGE_BG, stroke=None)\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=10,\n        titleFontSize=10,\n    )\n)\n\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}