{"spec_id":"heatmap-geographic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nheatmap-geographic: Geographic Heatmap for Spatial Density\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 81/100 | Updated: 2026-05-19\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\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 = \"#e8e8e8\" if THEME == \"light\" else \"#3A3A35\"\nLAND_STROKE = \"#999999\" if THEME == \"light\" else \"#6A6A60\"\n\n# Data - European cities with population-like density values\nnp.random.seed(42)\n\ncities = [\n    # (name, lat, lon, spread, n_points, weight_base)\n    (\"London\", 51.5, -0.1, 0.8, 80, 1.5),\n    (\"Paris\", 48.9, 2.3, 0.6, 70, 1.4),\n    (\"Berlin\", 52.5, 13.4, 0.7, 55, 1.2),\n    (\"Madrid\", 40.4, -3.7, 0.5, 50, 1.1),\n    (\"Rome\", 41.9, 12.5, 0.4, 45, 1.0),\n    (\"Vienna\", 48.2, 16.4, 0.4, 35, 0.9),\n    (\"Amsterdam\", 52.4, 4.9, 0.3, 40, 1.0),\n    (\"Brussels\", 50.8, 4.4, 0.3, 35, 0.9),\n    (\"Warsaw\", 52.2, 21.0, 0.5, 40, 0.8),\n    (\"Prague\", 50.1, 14.4, 0.3, 30, 0.8),\n    (\"Stockholm\", 59.3, 18.1, 0.4, 30, 0.7),\n    (\"Munich\", 48.1, 11.6, 0.3, 35, 0.9),\n    (\"Milan\", 45.5, 9.2, 0.4, 40, 1.0),\n    (\"Barcelona\", 41.4, 2.2, 0.4, 45, 1.0),\n    (\"Lisbon\", 38.7, -9.1, 0.4, 30, 0.8),\n]\n\ndata_rows = []\nfor _name, lat, lon, spread, n, weight in cities:\n    lats = np.random.normal(lat, spread, n)\n    lons = np.random.normal(lon, spread * 1.2, n)\n    values = np.random.exponential(weight, n) * 10\n    for i in range(n):\n        data_rows.append({\"latitude\": lats[i], \"longitude\": lons[i], \"value\": values[i]})\n\n# Add scattered rural points\nn_rural = 200\nrural_lats = np.random.uniform(36, 62, n_rural)\nrural_lons = np.random.uniform(-10, 25, n_rural)\nrural_values = np.random.exponential(0.3, n_rural) * 5\nfor i in range(n_rural):\n    data_rows.append({\"latitude\": rural_lats[i], \"longitude\": rural_lons[i], \"value\": rural_values[i]})\n\ndf = pd.DataFrame(data_rows)\n\n# Load world countries from natural earth (Vega datasets URL)\ncountries_url = \"https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json\"\ncountries = alt.topo_feature(countries_url, \"countries\")\n\n# Basemap layer - theme-adaptive country fills\nbasemap = (\n    alt.Chart(countries)\n    .mark_geoshape(fill=LAND_FILL, stroke=LAND_STROKE, strokeWidth=0.8)\n    .project(type=\"mercator\", scale=600, center=[10, 50])\n)\n\n# Heatmap layer - overlapping circles with viridis colormap\nheatmap_points = (\n    alt.Chart(df)\n    .mark_circle(opacity=0.5)\n    .encode(\n        longitude=\"longitude:Q\",\n        latitude=\"latitude:Q\",\n        size=alt.Size(\"value:Q\", scale=alt.Scale(range=[100, 1500]), legend=None),\n        color=alt.Color(\n            \"value:Q\",\n            scale=alt.Scale(scheme=\"viridis\", domain=[0, 30]),\n            legend=alt.Legend(\n                title=\"Density (%)\",\n                titleFontSize=18,\n                labelFontSize=14,\n                gradientLength=300,\n                gradientThickness=20,\n                orient=\"right\",\n            ),\n        ),\n        tooltip=[\n            alt.Tooltip(\"latitude:Q\", format=\".2f\", title=\"Lat\"),\n            alt.Tooltip(\"longitude:Q\", format=\".2f\", title=\"Lon\"),\n            alt.Tooltip(\"value:Q\", format=\".1f\", title=\"Value\"),\n        ],\n    )\n    .project(type=\"mercator\", scale=600, center=[10, 50])\n)\n\n# Combine layers with theme-adaptive chrome\nchart = (\n    alt.layer(basemap, heatmap_points)\n    .properties(\n        background=PAGE_BG,\n        width=1600,\n        height=900,\n        title=alt.Title(\n            \"European Activity Density · heatmap-geographic · python · altair · anyplot.ai\",\n            fontSize=28,\n            anchor=\"middle\",\n        ),\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0)\n    .configure_title(color=INK, fontSize=28)\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        titleColor=INK,\n        labelColor=INK_SOFT,\n        titleFontSize=18,\n        labelFontSize=14,\n        padding=12,\n        cornerRadius=4,\n    )\n)\n\n# Save outputs\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}